/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2015 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/*---------------------------------------------------------------------------*
 *                             Includes                                      *
 *---------------------------------------------------------------------------*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <math.h>
#include <cpl.h>
#include <muse.h>
#include "muse_exp_align_z.h"


typedef struct muse_indexpair muse_indexpair;

struct muse_indexpair {
    cpl_size first;
    cpl_size second;
};

typedef void (*muse_free_func)(void *);


/* Conversion factor degree to arcsecond */
static const double deg2as = 3600.;


/*---------------------------------------------------------------------------*
 *                             Functions code                                *
 *---------------------------------------------------------------------------*/

static void
muse_vfree(void **array, cpl_size size, muse_free_func deallocator)
{
  if (array) {
    cpl_size idx;
    for (idx = 0; idx < size; ++idx) {
      if (deallocator) {
        deallocator(array[idx]);
      }
    }
    cpl_free(array);
  }
  return;
}

static int
_muse_condition_less_than(double aValue1, double aValue2)
{
  return (aValue1 < aValue2) ? TRUE : FALSE;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Replace NaN values in a MUSE image.
  @param    aImage  the MUSE image to be cleaned
  @param    aValue  the numerical value to use for replacing NaN values
  @return   The return value is CPL_ERROR_NONE on success, and an
            appropriate error code otherwise.

  @error{set CPL_ERROR_NULL_INPUT\, return CPL_ERROR_NULL_INPUT, aImage is NULL}
  @error{set CPL_ERROR_TYPE_MISMATCH\, return CPL_ERROR_TYPE_MISMATCH,
         aImage does not have data section}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return CPL_ERROR_ILLEGAL_INPUT,
         aImage does not have data section}
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_utils_replace_nan(muse_image *aImage, float aValue)
{
  cpl_ensure(aImage, CPL_ERROR_NULL_INPUT, CPL_ERROR_NULL_INPUT);
  cpl_ensure(aImage->data, CPL_ERROR_ILLEGAL_INPUT, CPL_ERROR_ILLEGAL_INPUT);

  cpl_size npixel = cpl_image_get_size_x(aImage->data) *
      cpl_image_get_size_y(aImage->data);

  float *data = cpl_image_get_data_float(aImage->data);
  if (cpl_error_get_code() == CPL_ERROR_TYPE_MISMATCH) {
    cpl_error_set_message(__func__, CPL_ERROR_TYPE_MISMATCH,
                          "MUSE image data buffer has invalid type!");
    return CPL_ERROR_TYPE_MISMATCH;
  }

  cpl_size ipixel;
  for (ipixel = 0; ipixel < npixel; ++ipixel) {
    if (isnan(data[ipixel])) {
      data[ipixel] = aValue;
    }
  }

  if (aImage->stat) {
    npixel = cpl_image_get_size_x(aImage->stat) *
        cpl_image_get_size_y(aImage->stat);

    data = cpl_image_get_data_float(aImage->stat);
    if (cpl_error_get_code() == CPL_ERROR_TYPE_MISMATCH) {
      cpl_error_set_message(__func__, CPL_ERROR_TYPE_MISMATCH,
                            "MUSE image data buffer has invalid type!");
      return CPL_ERROR_TYPE_MISMATCH;
    }

    for (ipixel = 0; ipixel < npixel; ++ipixel) {
      if (isnan(data[ipixel])) {
        data[ipixel] = aValue;
      }
    }
  }

  return CPL_ERROR_NONE;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Load all FOV images into a MUSE image list.
  @param    aProcessing  the processing structure
  @return   a muse_imagelist or NULL on error
 */
/*----------------------------------------------------------------------------*/
static muse_imagelist *
muse_processing_fov_load_all(muse_processing *aProcessing)
{
  cpl_ensure(aProcessing, CPL_ERROR_NULL_INPUT, NULL);
  cpl_size nframes = cpl_frameset_get_size(aProcessing->inframes);
  cpl_ensure(nframes, CPL_ERROR_DATA_NOT_FOUND, NULL);

  muse_imagelist *fovimages = muse_imagelist_new();

  cpl_size iexposure = 0;
  cpl_size iframe;
  for (iframe = 0; iframe < nframes; ++iframe) {
    const cpl_frame *frame = cpl_frameset_get_position(aProcessing->inframes,
                                                       iframe);
    const char *tag = cpl_frame_get_tag(frame);

    if (muse_processing_check_intags(aProcessing, tag, strlen(tag))) {
      const char *filename = cpl_frame_get_filename(frame);

      cpl_msg_debug(__func__, "Loading FOV image '%s' as exposure %"
                    CPL_SIZE_FORMAT, filename, iexposure + 1);

      muse_image *image = muse_fov_load(filename);
      if (!image) {
        cpl_msg_error(__func__, "Could not load FOV image '%s'", filename);
        muse_imagelist_delete(fovimages);
        return NULL;
      }

      muse_imagelist_set(fovimages, image, iexposure);
      muse_processing_append_used(aProcessing, (cpl_frame *)frame,
                                  CPL_FRAME_GROUP_RAW, 1);
      ++iexposure;
    }
  }
  return fovimages;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Create a deep copy of a FOV image.
  @param    aImage  the image object to duplicate
  @return   a newly allocated duplicate of the input image or NULL on error

  The function is basically identical to muse_image_duplicate, except that
  it allows that the @c stat and @dq data members of the input image may
  be @c NULL. The latter is true for MUSE FOV images.
 */
/*----------------------------------------------------------------------------*/
static muse_image *
muse_fov_duplicate(muse_image *aImage)
{
  cpl_ensure(aImage, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(aImage->header && aImage->data, CPL_ERROR_ILLEGAL_INPUT, NULL);

  muse_image *image = muse_image_new();
  image->stat = NULL;
  image->dq = NULL;

  cpl_errorstate status = cpl_errorstate_get();
  image->header = cpl_propertylist_duplicate(aImage->header);
  image->data = cpl_image_duplicate(aImage->data);
  if (aImage->stat) {
    image->stat = cpl_image_duplicate(aImage->stat);
  }
  if (aImage->dq) {
    image->dq = cpl_image_duplicate(aImage->dq);
  }

  if (!cpl_errorstate_is_equal(status)) {
    muse_image_delete(image);
    return NULL;
  }
  return image;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Load source lists into an array of MUSE tables.
  @param    aProcessing  the processing structure
  @param    aPositions   the number of source lists to load
  @return   an array of MUSE tables or NULL on error

  To deallocate the returned array and all tables referenced by the array
  elements muse_vfree() should be used.
 */
/*----------------------------------------------------------------------------*/
static muse_table **
muse_processing_catalog_load_all(muse_processing *aProcessing,
                                 cpl_size *aPositions)
{
  cpl_ensure(aProcessing, CPL_ERROR_NULL_INPUT, NULL);
  cpl_size nframes = cpl_frameset_get_size(aProcessing->inframes);
  cpl_ensure(nframes, CPL_ERROR_DATA_NOT_FOUND, NULL);

  muse_table **srclists = cpl_calloc(nframes, sizeof *srclists);

  cpl_size iposition = 0;
  cpl_size iframe;
  for (iframe = 0; iframe < nframes; ++iframe) {
    const cpl_frame *frame = cpl_frameset_get_position(aProcessing->inframes,
                                                       iframe);
    const char *tag = cpl_frame_get_tag(frame);

    if (strncmp(tag, MUSE_TAG_SOURCE_LIST, 12) == 0) {
      const char *filename = cpl_frame_get_filename(frame);

      cpl_msg_debug(__func__, "Loading source list '%s'", filename);

      muse_table *table = muse_table_load(filename, 0);
      if (!table) {
        cpl_msg_error(__func__, "Could not load source list '%s'", filename);
        muse_vfree((void **)srclists, nframes, (muse_free_func)muse_table_delete);
        return NULL;
      }

      srclists[iposition] = table;
      muse_processing_append_used(aProcessing, (cpl_frame *)frame,
                                  CPL_FRAME_GROUP_CALIB, 1);
      ++iposition;
    }
  }
  cpl_msg_debug(__func__, "%" CPL_SIZE_FORMAT " source lists were loaded!",
                iposition);

  if (aPositions) {
    *aPositions = iposition;
  }
  return srclists;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Verify that a source list has the required structure.
  @param    aSourceCatalog  the source lists to validate.
  @return   @c 0 if the input source list is compliant, @c 1
            if a keyword is missing, and @c 2 if a table column is not present.
            On any other error @c -1 is returned.
 */
/*----------------------------------------------------------------------------*/
static int
muse_align_check_source_catalog(const muse_table *aSourceCatalog)
{
  cpl_ensure(aSourceCatalog, CPL_ERROR_NULL_INPUT, -1);

  const char *keywords[] =
  {
    "MJD-OBS",
    "DATE-OBS"
  };
  const cpl_size nkeys = sizeof keywords / sizeof keywords[0];

  const char *columns[] =
  {
    MUSE_SRCLIST_ID,
    MUSE_SRCLIST_X,
    MUSE_SRCLIST_Y,
    MUSE_SRCLIST_RA,
    MUSE_SRCLIST_DEC
  };
  const cpl_size ncolumns = sizeof columns / sizeof columns[0];

  cpl_size ikey;
  for (ikey = 0; ikey < nkeys; ++ikey) {
    const char *key = keywords[ikey];
    if (!cpl_propertylist_has(aSourceCatalog->header, key)) {
      cpl_msg_debug(__func__, "Keyword \"%s\" not found in input source "
                    "list header!", key);
      return 1;
    }
  }
  cpl_size icolumn;
  for (icolumn = 0; icolumn < ncolumns; ++icolumn) {
    const char *column = columns[icolumn];
    if (!cpl_table_has_column(aSourceCatalog->table, column)) {
      cpl_msg_debug(__func__, "Column \"%s\" not found in input source "
                    "list!", column);
      return 2;
    }
  }
  return 0;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Remove sources from a source list which are too close to a bad
            pixel.
  @param    aSourceList  the source lists to clean.
  @param    aFovImage    the FOV image associated to the source list.
  @return   The number of discarded sources, or @c -1 if an error occurred.
 */
/*----------------------------------------------------------------------------*/
static int
muse_align_clean_source_list(const muse_table *aSourceList,
                             muse_image *aFovImage, double aDistance)
{
  cpl_ensure(aSourceList && aFovImage, CPL_ERROR_NULL_INPUT, -1);
  cpl_ensure(aDistance >= 0., CPL_ERROR_ILLEGAL_INPUT, -1);

  const cpl_size distance = ceil(aDistance);
  const cpl_size dsquare = distance * distance;

  cpl_size nx = cpl_image_get_size_x(aFovImage->data);
  cpl_size ny = cpl_image_get_size_y(aFovImage->data);
  cpl_binary *bpm = cpl_mask_get_data(cpl_image_get_bpm(aFovImage->data));

  cpl_table *srclist = aSourceList->table;
  cpl_table_unselect_all(srclist);

  cpl_size nsources = cpl_table_get_nrow(srclist);
  cpl_size isource;
  for (isource = 0; isource < nsources; ++isource) {
    double xpos = cpl_table_get_double(srclist, MUSE_SRCLIST_X, isource, 0);
    double ypos = cpl_table_get_double(srclist, MUSE_SRCLIST_Y, isource, 0);

    cpl_size nbad = 0;
    cpl_size iy;
    for (iy = 0; iy < distance; ++iy) {
      cpl_size ix;
      for (ix = 0; ix < distance; ++ix) {
        if (ix * ix + iy * iy < dsquare) {
          cpl_size x0 = xpos - ix;
          cpl_size x1 = xpos + ix;
          cpl_size y0 = ypos - iy;
          cpl_size y1 = ypos + iy;

          if ((x0 < 0) || (y0 < 0) || (x1 >= nx) || (y1 >= ny)) {
            continue;
          }

          cpl_size k00 = y0 * nx + x0;
          cpl_size k01 = y0 * nx + x1;
          cpl_size k10 = y1 * nx + x0;
          cpl_size k11 = y1 * nx + x1;

          if (bpm[k00] || bpm[k01] || bpm[k10] || bpm[k11]) {
            ++nbad;
          }
        }
      }
    }
    if (nbad) {
      cpl_table_select_row(srclist, isource);
    }

  }

  cpl_size ndiscarded = cpl_table_count_selected(srclist);
  cpl_table_erase_selected(srclist);
  cpl_table_select_all(srclist);

  cpl_propertylist_update_int(aSourceList->header, QC_ALIGN_NDET,
                              cpl_table_get_nrow(srclist));

  return ndiscarded;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Validate the exposure alignment task parameters.
  @param    aParams  object with parameters to validate
  @return   CPL_TRUE if everything is ok, CPL_FALSE if not.
 */
/*----------------------------------------------------------------------------*/
static cpl_boolean
muse_align_check_detection_params(muse_exp_align_params_t *aParams)
{
  if (aParams->bkgignore < 0.) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT,
                          "Fraction of image pixel to ignore for the "
                          "background estimation must be greater than"
                          "or equal to zero!");
    return CPL_FALSE;
  }
  if (aParams->bkgfraction <= 0.) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT,
                          "Fraction of image pixels to use for the "
                          "background estimation must be greater "
                          "than zero!");
    return CPL_FALSE;
  }
  if (aParams->fwhm < 0.5) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT,
                          "FWHM must be greater than 0.5 pixels!");
    return CPL_FALSE;
  }
  if (aParams->srcmax <= aParams->srcmin) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT,
                          "Maximum number of sources must be grater than the "
                          "minimum number of sources!");
    return CPL_FALSE;
  }
  if (aParams->roundmax < aParams->roundmin) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT,
                          "Upper limit of the point-source roundness must be "
                          "greater than the lower limit!");
    return CPL_FALSE;
  }
  if (aParams->sharpmin <= 0.) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT,
                          "Low limit of the point-source sharpness must be "
                          "greater than zero!");
    return CPL_FALSE;
  }
  if (aParams->sharpmax < aParams->sharpmin) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT,
                          "Upper limit of the point-source sharpness must be "
                          "greater than the lower limit!");
    return CPL_FALSE;
  }
  return CPL_TRUE;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Parse a comma separated list of values into an array.
  @param    aValuelist  the string encoded list of values.
  @return   the array object containing the parsed value on success, and NULL
            otherwise.
 */
/*----------------------------------------------------------------------------*/
static cpl_array *
muse_align_parse_valuelist(const char *aValuelist)
{
  cpl_ensure(aValuelist, CPL_ERROR_NULL_INPUT, NULL);
  if (strlen(aValuelist) == 0) {
    cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND,
                          "List of values is empty!");
    return NULL;
  }

  cpl_array *strings = muse_cplarray_new_from_delimited_string(aValuelist, ",");
  cpl_size nvalues = cpl_array_get_size(strings);

  cpl_array *values  = cpl_array_new(nvalues, CPL_TYPE_DOUBLE);
  cpl_size ivalue;
  for (ivalue = 0; ivalue < nvalues; ++ivalue) {
    const char *svalue = cpl_array_get_string(strings, ivalue);
    if ((strncasecmp(svalue, "inf", 3) == 0) ||
        (strncasecmp(svalue, "nan", 3) == 0)) {
      cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT,
                            "Illegal value \"%s\" encountered!", svalue);
      cpl_array_delete(values);
      cpl_array_delete(strings);
      return NULL;
    }
    else {
      char *last = NULL;
      double value = strtod(svalue, &last);
      if( (errno == ERANGE) || ((value == 0.) && (last == svalue))) {
        cpl_error_set_message(__func__, CPL_ERROR_TYPE_MISMATCH,
                              "Could not convert \"%s\" to a decimal number!", svalue);
        cpl_array_delete(values);
        cpl_array_delete(strings);
        return NULL;
      }
      cpl_array_set_double(values, ivalue, value);
    }
  }
  cpl_array_delete(strings);
  return values;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Check whether the input header contains a gnomonic WCS.
  @param    aHeader    the header to check
  @return   CPL_TRUE if a gnomonic WCS is present, or CPL_FALSE otherwise.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         One or more arguments are NULL}

  The property list aHeader is searched for a gnomonic WCS , i.e. the keywords
  CTYPE1 and CTYPE2 must be present and their values must be set to
  "RA---TAN" and "DEC--TAN", respectively.
 */
/*----------------------------------------------------------------------------*/
static cpl_boolean
muse_align_wcs_is_gnomonic(cpl_propertylist *aHeader)
{
  cpl_ensure(aHeader, CPL_ERROR_NULL_INPUT, CPL_FALSE);
  const char *ctype1 = muse_pfits_get_ctype(aHeader, 1);
  const char *ctype2 = muse_pfits_get_ctype(aHeader, 2);

  if ((ctype1 && ctype2) &&
      (strncmp(ctype1, "RA---TAN", 9) == 0) &&
      (strncmp(ctype2, "DEC--TAN", 9) == 0)) {
    return CPL_TRUE;
  }

  return CPL_FALSE;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Compute celestial coordinates from pixel positions.
  @param    aTable      the table containing the pixel positions of the sources.
  @param    aWCSHeader  the property list containing WCS definition.
  @return   0 if everything is ok, -1 something went wrong

 */
/*----------------------------------------------------------------------------*/
static int
muse_align_celestial_from_pixel(cpl_table *aTable,
                                cpl_propertylist *aWCSHeader)
{
  muse_wcs *wcs = muse_wcs_new(aWCSHeader);

  const char *unit1 = muse_pfits_get_cunit(aWCSHeader, 1);
  const char *unit2 = muse_pfits_get_cunit(aWCSHeader, 2);

  if (unit1 && unit2) {
    if ((strncmp(unit1, unit2, 4) == 0) && (strncmp(unit1, "deg", 3) == 0)) {
      wcs->iscelsph = CPL_TRUE;
    }
  } else {
    return -1;
  }

  cpl_errorstate status = cpl_errorstate_get();

  cpl_table_new_column(aTable, "RA", CPL_TYPE_DOUBLE);
  cpl_table_new_column(aTable, "DEC", CPL_TYPE_DOUBLE);

  cpl_size irow;
  for (irow = 0; irow < cpl_table_get_nrow(aTable); ++irow) {
    double x = cpl_table_get_double(aTable, "X", irow, NULL);
    double y = cpl_table_get_double(aTable, "Y", irow, NULL);
    double ra;
    double dec;
    muse_wcs_celestial_from_pixel_fast(wcs, x, y, &ra, &dec);

    cpl_table_set_double(aTable, "RA", irow, ra);
    cpl_table_set_double(aTable, "DEC", irow, dec);
  }
  cpl_free(wcs);

  if (!cpl_errorstate_is_equal(status)) {
    return -1;
  }
  return 0;
}

/* XXX: Keep the conditional until the SVD code has been fully verified. *
 *      Once this is done, completely replace the old code with the SVD  *
 *      version.                                                         */
#define USE_SVD_SOLVER
#ifdef USE_SVD_SOLVER

#if CPL_VERSION_CODE < 459008
#define cpl_matrix_solve_svd muse_cplmatrix_solve_svd

/*
 * The following implementation of one-sided Jacobi orthogonalization
 * to compute the SVD of a MxN matrix, with M >= N is based on the
 * implementation found in the GNU Scientific Library (GSL).
 */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Compute the singular value decomposition of a real matrix using
            Jacobi plane rotations.
  @param U  The M x N matrix to be factorized.
  @param S  A vector of N elements for storing the singular values.
  @param V  An N x N square matrix for storing the right singular vectors.
  @return   @c CPL_ERROR_NONE if the factorization was successful and an error
            code otherwise.
 
  @error{set CPL_ERROR_NULL_INPUT\, return CPL_ERROR_NULL_INPUT,
         One or more arguments are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return CPL_ERROR_ILLEGAL_INPUT,
         the matrix U has less rows than columns (M < N), or the matrix V
         is not a square matrix}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return CPL_ERROR_INCOMPATIBLE_INPUT,
         the number of rows of the matrix V does not match the number
         of columns of U (N), or the size of the vector S does not match
         the number of rows of U (M)}
 
  The function computes singular value decomposition (SVD) of a real M x N
  matrix @em U, where M >= N. It uses a one-sided Jacobi orthogonalization
  as described in James Demmel, Kresimir Veselic, "Jacobi's Method is more
  accurate than QR", Lapack Working Note 15 (LAWN15), October 1989,
  Algorithm 4.1 which is available at @em netlib.org.

  The input matrix @em U is overwritten during the process and contains
  in the end the left singular vectors of @em U. The matrix @em V will be
  updated with the right singular vectors of @em U, and the vector @em S
  will contain the singular values. For efficiency reasons the singular
  values are not written to the diagonal elements of a N x N diagonal matrix,
  but an N-elements vector.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_cplmatrix_decomp_sv_jacobi(cpl_matrix *U, cpl_vector *S, cpl_matrix *V)
{
  cpl_ensure_code((U != NULL) && (V != NULL) && (S != NULL),
                  CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(cpl_matrix_get_nrow(U) >= cpl_matrix_get_ncol(U),
                  CPL_ERROR_ILLEGAL_INPUT);
  cpl_ensure_code(cpl_matrix_get_nrow(V) == cpl_matrix_get_ncol(V),
                  CPL_ERROR_ILLEGAL_INPUT);
  cpl_ensure_code(cpl_matrix_get_nrow(V) == cpl_matrix_get_ncol(U),
                  CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_ensure_code(cpl_vector_get_size(S) == cpl_matrix_get_ncol(U),
                  CPL_ERROR_INCOMPATIBLE_INPUT);

  const cpl_size m = cpl_matrix_get_nrow(U);
  const cpl_size n = cpl_matrix_get_ncol(U);

  /* Initialize the rotation counter and the sweep counter. *
   * Use a minimum number of 12 sweeps.                     */
  cpl_size count = 1;
  cpl_size sweep = 0;
  cpl_size sweepmax = CPL_MAX(5 * n, 12);

  double tolerance = 10. * m * DBL_EPSILON;

  /* Fill V with the identity matrix. */
  cpl_matrix_fill(V, 0.);
  cpl_matrix_fill_diagonal(V, 1., 0);

  /* Store the column error estimates in S, for use during the *
   * orthogonalization.                                        */

  cpl_size i;
  cpl_size j;
  cpl_vector *cv1 = cpl_vector_new(m);
  double *const _cv1 = cpl_vector_get_data(cv1);

  for (j = 0; j < n; ++j) {
    for (i = 0; i < m; ++i) {
      _cv1[i] = cpl_matrix_get(U, i, j);
    }

    double s = sqrt(cpl_vector_product(cv1, cv1));
    cpl_vector_set(S, j, DBL_EPSILON * s);
  }

  /* Orthogonalization of U by plane rotations. */
  cpl_vector *cv2 = cpl_vector_new(m);
  double *const _cv2 = cpl_vector_get_data(cv2);

  while ((count > 0) && (sweep <= sweepmax)) {

    /* Initialize the rotation counter. */
    count = n * (n - 1) / 2;

    for (j = 0; j < n - 1; ++j) {
      cpl_size k;
      for (k = j + 1; k < n; ++k) {
        for (i = 0; i < m; ++i) {
          _cv1[i] = cpl_matrix_get(U, i, j);
        }
        for (i = 0; i < m; ++i) {
          _cv2[i] = cpl_matrix_get(U, i, k);
        }

        /* Compute the (j, k) submatrix elements of Ut*U. The *
         * non-diagonal element c is scaled by 2.             */
        double a = sqrt(cpl_vector_product(cv1, cv1));
        double b = sqrt(cpl_vector_product(cv2, cv2));
        double c = 2. * cpl_vector_product(cv1, cv2);

        /* Test whether columns j, k are orthogonal, or dominant errors. */
        double abserr_a = cpl_vector_get(S, j);
        double abserr_b = cpl_vector_get(S, k);
        cpl_boolean orthogonal = (fabs(c) <= tolerance * a * b);
        cpl_boolean sorted     = (a >= b);
        cpl_boolean noisy_a    = (a < abserr_a);
        cpl_boolean noisy_b    = (b < abserr_b);

        if (sorted && (orthogonal || noisy_a || noisy_b)) {
          --count;
          continue;
        }

        /* Compute the Jacobi rotation which diagonalizes the (j, k) *
         * submatrix.                                                */
        double q = a * a - b * b;
        double v = sqrt(c * c + q * q);
        double cs;  /* cos(theta) */
        double sn;  /* sin(theta) */

        if (v == 0. || !sorted) {
          cs = 0.;
          sn = 1.;
        } else {
          cs = sqrt((v + q) / (2. * v));
          sn = c / (2. * v * cs);
        }

        /* Update the columns j and k of U and V */
        for (i = 0; i < m; ++i) {
          const double u_ij = cpl_matrix_get(U, i, j);
          const double u_ik = cpl_matrix_get(U, i, k);
          cpl_matrix_set(U, i, j,  u_ij * cs + u_ik * sn);
          cpl_matrix_set(U, i, k, -u_ij * sn + u_ik * cs);
        }

        for (i = 0; i < n; ++i) {
          const double v_ij = cpl_matrix_get(V, i, j);
          const double v_ik = cpl_matrix_get(V, i, k);
          cpl_matrix_set(V, i, j,  v_ij * cs + v_ik * sn);
          cpl_matrix_set(V, i, k, -v_ij * sn + v_ik * cs);
        }

        cpl_vector_set(S, j, fabs(cs) * abserr_a +
            fabs(sn) * abserr_b);
        cpl_vector_set(S, k, fabs(sn) * abserr_a +
            fabs(cs) * abserr_b);
      }
    }
    ++sweep;

  }
  cpl_vector_delete(cv2);

  /* Compute singular values. */
  double prev_norm = -1.;
  for (j = 0; j < n; ++j) {
    for (i = 0; i < m; ++i) {
      _cv1[i] = cpl_matrix_get(U, i, j);
    }

    double norm = sqrt(cpl_vector_product(cv1, cv1));

    /* Determine if the singular value is zero, according to the  *
     * criteria used in the main loop above (i.e. comparison with *
     * norm of previous column).                                  */
    if ((norm == 0.) || (prev_norm == 0.) ||
        ((j > 0) && (norm <= tolerance * prev_norm))) {
      cpl_vector_set(S, j, 0.);          /* singular */
      cpl_matrix_fill_column(U, 0., j);  /* annihilate column */
      prev_norm = 0.;
    } else {
      cpl_vector_set(S, j, norm);     /* non-singular */

      /* Normalize the column vector of U */
      for (i = 0; i < m; ++i) {
        cpl_matrix_set(U, i, j, _cv1[i] / norm);
      }
      prev_norm = norm;
    }
  }
  cpl_vector_delete(cv1);

  if (count > 0) {
    return CPL_ERROR_CONTINUE;
  }
  return CPL_ERROR_NONE;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Solve a linear system in a least square sense using an
          SVD factorization.
  @param  coeff Linear system coefficients
  @param  rhs   Right hand side of the linear system
  @return the solution vector or NULL on error
 */
/*----------------------------------------------------------------------------*/
static cpl_matrix *
muse_cplmatrix_solve_svd(const cpl_matrix *coeff, const cpl_matrix *rhs)
{
  cpl_ensure((coeff != NULL) && (rhs != NULL), CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(cpl_matrix_get_nrow(coeff) == cpl_matrix_get_nrow(rhs),
             CPL_ERROR_INCOMPATIBLE_INPUT, NULL);
  cpl_ensure(cpl_matrix_get_ncol(rhs) == 1, CPL_ERROR_ILLEGAL_INPUT, NULL);

  /* Compute the SVD factorization of the coefficient matrix */
  cpl_matrix *U = cpl_matrix_duplicate(coeff);
  cpl_size nc = cpl_matrix_get_ncol(U);

  cpl_matrix *V = cpl_matrix_new(nc, nc);
  cpl_vector *S = cpl_vector_new(nc);

  cpl_error_code status = muse_cplmatrix_decomp_sv_jacobi(U, S, V);
  if (status != CPL_ERROR_NONE) {
    cpl_vector_delete(S);
    cpl_matrix_delete(V);
    cpl_matrix_delete(U);
    cpl_error_set_where(cpl_func);
    return NULL;
  }

  /* Solve the linear system */
  cpl_matrix *Ut = cpl_matrix_transpose_create(U);
  cpl_matrix *w  = cpl_matrix_product_create(Ut, rhs);
  cpl_matrix_delete(Ut);

  const double *_s = cpl_vector_get_data_const(S);
  double *_w = cpl_matrix_get_data(w);
  cpl_size ic;

  for (ic = 0; ic < nc; ++ic) {
    double s = _s[ic];
    if (s != 0.) {
      s = 1. / s;
    }
    _w[ic] *= s;
  }

  cpl_matrix *solution = cpl_matrix_product_create(V, w);
  cpl_matrix_delete(w);
  cpl_vector_delete(S);
  cpl_matrix_delete(V);
  cpl_matrix_delete(U);
  return solution;
}

#endif

#endif

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief Compute the least-squares solution of a linear matrix equation.
  @param  aMatrix  the left-hand side MxN coefficient matrix.
  @param  aVector  the right-hand side MxK ordinate matrix.
  @return a newly allocated NxK least-squares solution matrix.

 */
/*----------------------------------------------------------------------------*/
static cpl_matrix *
muse_cplmatrix_solve_least_square(const cpl_matrix *aMatrix1,
                                  const cpl_matrix *aMatrix2)
{
  cpl_ensure(aMatrix1 && aMatrix2, CPL_ERROR_NULL_INPUT, NULL);

  cpl_size nc = cpl_matrix_get_ncol(aMatrix1);
  cpl_size nr = cpl_matrix_get_nrow(aMatrix1);
  cpl_ensure(cpl_matrix_get_nrow(aMatrix2) == nr,
             CPL_ERROR_INCOMPATIBLE_INPUT, NULL);

  cpl_matrix *result = NULL;
  cpl_errorstate status = cpl_errorstate_get();

#ifndef USE_SVD_SOLVER
  if (nr == nc) {
    result = cpl_matrix_solve(aMatrix1, aMatrix2);
  } else {
    /* Handle over- and under-determined systems */
    if (nr > nc) {
      result = cpl_matrix_solve_normal(aMatrix1, aMatrix2);
    } else {
      cpl_matrix *mt = cpl_matrix_transpose_create(aMatrix1);
      cpl_matrix *mgram = cpl_matrix_product_create(aMatrix1, mt);
      cpl_matrix *mw = cpl_matrix_solve(mgram, aMatrix2);

      result = cpl_matrix_product_create(mt, mw);
      cpl_matrix_delete(mw);
      cpl_matrix_delete(mgram);
      cpl_matrix_delete(mt);
    }
  }
#else
  if (nr >= nc) {
    result = cpl_matrix_solve_svd(aMatrix1, aMatrix2);
  } else {
    cpl_matrix *mt = cpl_matrix_transpose_create(aMatrix1);
    cpl_matrix *mgram = cpl_matrix_product_create(aMatrix1, mt);
    cpl_matrix *mw = cpl_matrix_solve(mgram, aMatrix2);

    result = cpl_matrix_product_create(mt, mw);
    cpl_matrix_delete(mw);
    cpl_matrix_delete(mgram);
    cpl_matrix_delete(mt);
  }
#endif

  if (status != cpl_errorstate_get()) {
    cpl_matrix_delete(result);
    result = NULL;
  }
  return result;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief Compute the cosine of matrix elements.
  @param aMatrix  the matrix.
  @return CPL_ERROR_NONE on success or an appropriate error code on failure

  @error{set CPL_ERROR_NULL_INPUT\, return CPL_ERROR_NULL_INPUT, aMatrix is NULL}

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_cplmatrix_cosine(cpl_matrix *aMatrix)
{
  cpl_ensure(aMatrix, CPL_ERROR_NULL_INPUT, CPL_ERROR_NULL_INPUT);

  cpl_size size = cpl_matrix_get_nrow(aMatrix) * cpl_matrix_get_ncol(aMatrix);
  double *mdata = cpl_matrix_get_data(aMatrix);

  cpl_size i;
  for (i = 0; i < size; ++i) {
    mdata[i] = cos(mdata[i]);
  }
  return CPL_ERROR_NONE;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Estimate the background level of an exposure and its uncertainty.
  @param    aImage        The image to analyse.
  @param    aMinFraction  Matrix of declination offsets.
  @param    aFraction     Matrix of mean declination.
  @param    bkgValue      Optional, additional right ascension offset.
  @param    bkgError      Optional, additional right ascension offset.
  @return   A matrix containing the computed distances, or NULL.

  The function computes for each matrix element the angular distance from
  the given position and differences in right ascension and declination.
  Extra shift parameters may be given. They are completely ignored if they are
  set to NULL, otherwise they are applied as extra offsets in the computation.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_align_estimate_background(muse_image *aImage,
                               double aMinFraction, double aFraction,
                               double *bkgValue, double *bkgError)
{

  cpl_ensure(aImage, CPL_ERROR_NULL_INPUT, CPL_ERROR_NULL_INPUT);
  cpl_ensure((aImage->data && aImage->header),
             CPL_ERROR_ILLEGAL_INPUT, CPL_ERROR_ILLEGAL_INPUT);

  muse_image *image = muse_image_new();

  image->data = cpl_image_duplicate(aImage->data);
  image->header = cpl_propertylist_duplicate(aImage->header);


  /* A FOV image may have pixels set to NaN, which should be ignored *
   * for estimating the background. Also set an empty DQ image so    *
   * that muse_sky_create_skymask() does not raise an error.         */
  cpl_image_reject_value(image->data, CPL_VALUE_NOTFINITE);
  image->dq = cpl_image_new(cpl_image_get_size_x(image->data),
                            cpl_image_get_size_y(image->data),
                            CPL_TYPE_INT);
  muse_mask *mask = muse_sky_create_skymask(image, aMinFraction, aFraction,
                                            "ESO QC EXPALIGN BKG");
  if (!mask) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_OUTPUT,
                          "Creating background mask failed!");
    return CPL_ERROR_ILLEGAL_OUTPUT;
  }

  cpl_mask_not(mask->mask); /* invert mask to reject non-sky pixels */
  cpl_image_reject_from_mask(image->data, mask->mask);
  muse_mask_delete(mask);

  double bkg_sigma = 0.;
  double bkg = cpl_image_get_mad(image->data, &bkg_sigma);

  muse_image_delete(image);

  *bkgValue = bkg;
  *bkgError = bkg_sigma;

  return CPL_ERROR_NONE;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Load and setup the source position catalogs for a given set of
            field-of-view images.
  @param    aProcessing  the processing structure
  @param    aImagelist   the list of FOV images for which the source lists
                         should be created
  @return   An array of MUSE tables containing the source positions for
            each input field-of-view image, or @c NULL in case of errors.

  The source catalogs are loaded from the input data set provided by the
  @em aProcessing structure. There must be one input source catalog for
  each input FOV image.

  The result array contains the loaded source catalogs in the same order
  as the input FOV images are ordered, i.e. the image and the source positions
  are located at the same index position. The source lists are setup in such
  a way that they can be used for the field offset computation.
 */
/*----------------------------------------------------------------------------*/
static muse_table **
muse_align_load_catalogs(muse_processing *aProcessing,
                         muse_imagelist *aImagelist)
{

  cpl_ensure(aProcessing && aImagelist, CPL_ERROR_NULL_INPUT, NULL);

  cpl_size nexposures = muse_imagelist_get_size(aImagelist);
  cpl_size npositions = 0;

  cpl_msg_info(__func__, "Loading source catalogs");
  muse_table **_srclists =
      muse_processing_catalog_load_all(aProcessing, &npositions);
  if (!_srclists) {
    cpl_msg_error(__func__, "Could not load input source catalogs!");
    return NULL;
  }

  if (npositions != nexposures) {
    muse_vfree((void **)_srclists, npositions, (muse_free_func)muse_table_delete);
    cpl_msg_error(__func__, "Number of input source catalogs does not match "
                    "the number of FOV images! Expected %" CPL_SIZE_FORMAT
                    ", found %" CPL_SIZE_FORMAT "!", nexposures, npositions);
    return NULL;
  }

  cpl_msg_info(__func__, "Validating input source catalogs");
  cpl_size iexposure;
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    const muse_table *_srclist = _srclists[iexposure];
    int invalid = muse_align_check_source_catalog(_srclist);
    if (invalid) {
      switch (invalid) {
        case 1:
          cpl_msg_error(__func__, "Input source catalog %" CPL_SIZE_FORMAT
                        " is invalid: required keyword is missing!",
                        iexposure);
          break;
        case 2:
          cpl_msg_error(__func__, "Input source catalog %" CPL_SIZE_FORMAT
                        " is invalid: required table column is missing!",
                        iexposure);
          break;
        default:
          break;
      }
      muse_vfree((void **)_srclists, npositions, (muse_free_func)muse_table_delete);
      return NULL;
    }
  }

  /* For each input FOV find the matching input source list. If a   *
   * match is found it is added to the final source list array such *
   * that the FOV images and their corresponding source list are    *
   * accessible through the same array index.                       */

  muse_table **srclists = cpl_calloc(nexposures, sizeof *srclists);
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    const muse_image *fov = muse_imagelist_get(aImagelist, iexposure);
    double mjdobs = muse_pfits_get_mjdobs(fov->header);

    cpl_size jexposure;
    for (jexposure = 0; jexposure < nexposures; ++jexposure) {
      muse_table *_srclist = _srclists[jexposure];
      if (_srclist) {
        double _mjdobs = muse_pfits_get_mjdobs(_srclist->header);

        if (_mjdobs == mjdobs) {
          srclists[iexposure] = _srclist;

          /* XXX: Invalidate the entry so that it is not hit again. *
           *      An attempt to be robust against duplicate mjdobs  *
           *      values.                                          */
          _srclists[jexposure] = NULL;
        }
      }
    }

    if (!srclists[iexposure]) {
      muse_vfree((void **)_srclists, npositions, (muse_free_func)muse_table_delete);
      muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
      cpl_msg_error(__func__, "No input source catalog was found for FOV image %"
                    CPL_SIZE_FORMAT, iexposure + 1);
      return NULL;
    }

    /* Final setup of the input source list so that it can be used  *
     * for calculating the field offsets. If the WCS coordinates of *
     * the source positions are present, i.e. the corresponding     *
     * table columns exist, they are reset using the pixel position *
     * and the WCS coordinate system of the associated FOV image.    */
    cpl_table *catalog = srclists[iexposure]->table;
    if (cpl_table_has_column(catalog, MUSE_SRCLIST_RA)) {
      cpl_table_erase_column(catalog, MUSE_SRCLIST_RA);
    }
    if (cpl_table_has_column(catalog, MUSE_SRCLIST_DEC)) {
      cpl_table_erase_column(catalog, MUSE_SRCLIST_DEC);
    }

    /* Compute right ascension and declination from the pixel coordinates of *
     * each detected point-source using the WCS information of each exposure */
    if (muse_align_celestial_from_pixel(catalog, fov->header)) {
      muse_vfree((void **)_srclists, npositions, (muse_free_func)muse_table_delete);
      muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
      cpl_msg_error(__func__, "Computing WCS coordinates of catalog sources "
                    "from pixel positions failed for image %" CPL_SIZE_FORMAT
                    " of %" CPL_SIZE_FORMAT, iexposure + 1, nexposures);
      return NULL;
    }

    cpl_size nsources = cpl_table_get_nrow(catalog);
    cpl_table *srclist = muse_cpltable_new(muse_source_list_def, nsources);

    cpl_size isource;
    for (isource = 0; isource < nsources; ++isource) {
      double value = 0.;
      int invalid = 0;

      cpl_table_set_int(srclist, MUSE_SRCLIST_ID, isource, isource + 1);

      value = cpl_table_get_double(catalog, MUSE_SRCLIST_X,
                                   isource, &invalid);
      cpl_table_set_double(srclist, MUSE_SRCLIST_X, isource, value);
      value = cpl_table_get_double(catalog, MUSE_SRCLIST_Y,
                                   isource, &invalid);
      cpl_table_set_double(srclist, MUSE_SRCLIST_Y, isource, value);
      cpl_table_set_invalid(srclist, MUSE_SRCLIST_FLUX, isource);
      cpl_table_set_invalid(srclist, MUSE_SRCLIST_SHARPNESS, isource);
      cpl_table_set_invalid(srclist, MUSE_SRCLIST_ROUNDNESS, isource);
      value = cpl_table_get_double(catalog, MUSE_SRCLIST_RA,
                                   isource, &invalid);
      cpl_table_set_double(srclist, MUSE_SRCLIST_RA, isource, value);
      cpl_table_set_double(srclist, MUSE_SRCLIST_RACORR, isource, value);
      value = cpl_table_get_double(catalog, MUSE_SRCLIST_DEC,
                                   isource, &invalid);
      cpl_table_set_double(srclist, MUSE_SRCLIST_DEC, isource, value);
      cpl_table_set_double(srclist, MUSE_SRCLIST_DECCORR, isource, value);

    }
    srclists[iexposure]->table = srclist;
    cpl_table_delete(catalog);

    cpl_propertylist_erase_regexp(srclists[iexposure]->header,
                                      "^(BUNIT|ESO QC.*)", CPL_FALSE);

    cpl_propertylist_update_string(srclists[iexposure]->header,
                                   QC_ALIGN_SRC_POSITIONS, "catalog");
    cpl_propertylist_update_int(srclists[iexposure]->header,
                                QC_ALIGN_NDET, cpl_table_get_nrow(srclist));
    cpl_propertylist_update_float(srclists[iexposure]->header,
                                  QC_ALIGN_BKG_MEDIAN, 0.);
    cpl_propertylist_update_float(srclists[iexposure]->header,
                                  QC_ALIGN_BKG_MAD, 0.);
    cpl_propertylist_update_float(srclists[iexposure]->header,
                                  QC_ALIGN_THRESHOLD, 0.);
  }
  muse_vfree((void **)_srclists, nexposures, (muse_free_func)muse_table_delete);

  return srclists;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Detect point-sources on each image in the input image list.
  @param    aImagelist the list of input images
  @param    aParams    the parameters list with valid source detection parameters
  @return   An array of MUSE tables with one source catalog for each input image,
            or @c NULL if an error occurred.
  The returned array has the same size as the input image list, and has the
  same order as the list of input images, i.e. the input image and its
  corresponding list of detected sources can be found at the same index position
  to comply with the data layout assumed by the field offset computation.
 */
/*----------------------------------------------------------------------------*/
static muse_table **
muse_align_detect_sources(muse_imagelist *aImagelist, muse_exp_align_params_t *aParams)
{
  cpl_ensure((aImagelist && aParams), CPL_ERROR_NULL_INPUT, NULL);

  /* For each FOV input image run the source detection and collect *
   * the created source lists                                      */
  int nobjects[2] = {aParams->srcmin, aParams->srcmax};

  double roundness[2] = {aParams->roundmin, aParams->roundmax};
  double sharpness[2] = {aParams->sharpmin, aParams->sharpmax};

  cpl_size nexposures = muse_imagelist_get_size(aImagelist);
  muse_table **srclists = cpl_calloc(nexposures, sizeof *srclists);

  cpl_size iexposure;
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    cpl_msg_debug(__func__, "Detecting sources on image %" CPL_SIZE_FORMAT
                  " of %" CPL_SIZE_FORMAT, iexposure + 1, nexposures);

    muse_image *fov = muse_imagelist_get(aImagelist, iexposure);

    srclists[iexposure] = muse_table_new();

    /* Estimate FOV sky background for use as detection threshold */
    double bkg_sigma = 0.;
    double bkg = 0.;
    double threshold = aParams->threshold;

    muse_align_estimate_background(fov, aParams->bkgignore,
                                   aParams->bkgfraction, &bkg, &bkg_sigma);

    cpl_msg_info(__func__, "Median background level computed from sky mask: "
                 "%.6f +/- %.6f (MAD)", bkg, bkg_sigma);

    if (aParams->threshold <= 0.) {
      threshold = bkg + fabs(threshold) * bkg_sigma;
    }

    cpl_boolean iterate = CPL_TRUE;
    int iteration = 0;
    cpl_size ndetections = 0;
    cpl_table *detections;
    cpl_image *image = cpl_image_cast(fov->data, CPL_TYPE_DOUBLE);

    /* If the image does not have an even number of pixels along the *
     * any axis discard the last column and row respectively.         */

    /* XXX: The size limit along the x- and y-axis corresponds to    *
     *      minimum convolution box size of muse_find_stars(). This  *
     *      check should better be done in muse_find_stars() itself! */
    cpl_size nx = cpl_image_get_size_x(image);
    cpl_size ny = cpl_image_get_size_y(image);

    if ((nx < 6) || (ny < 6)) {
      cpl_image_delete(image);
      muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
      cpl_msg_error(__func__, "Image %" CPL_SIZE_FORMAT " of %"
                    CPL_SIZE_FORMAT "is too small!", iexposure + 1, nexposures);
      return NULL;
    }

    if ((nx % 2 != 0) || (ny % 2 != 0)) {
      cpl_msg_debug(__func__, "Trimming image %" CPL_SIZE_FORMAT " to an "
                    "even number of pixels along the axes.", iexposure + 1);
      if (nx % 2 != 0) {
        --nx;
      }
      if (ny % 2 != 0) {
        --ny;
      }
      cpl_image *_image = cpl_image_extract(image, 1, 1, nx, ny);
      if (!_image) {
        cpl_image_delete(image);
        muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
        cpl_msg_error(__func__, "Trimming image %" CPL_SIZE_FORMAT " failed",
                      iexposure + 1);
        return NULL;
      }
      cpl_image_delete(image);
      image = _image;
    } /* if odd image dimension(s) */

    while (iterate) {
      cpl_errorstate status = cpl_errorstate_get();

      cpl_msg_info(__func__, "Searching for sources using pixel value %.6f "
                   "as detection threshold", threshold);

      detections = muse_find_stars(image, threshold, aParams->fwhm,
                                   roundness, sharpness);

      if (!detections) {
        if (cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND) {
          cpl_errorstate_set(status);
          ndetections = 0;
        } else {
          cpl_image_delete(image);
          muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
          cpl_msg_error(__func__, "Source detection failed on image %"
                        CPL_SIZE_FORMAT " of %" CPL_SIZE_FORMAT, iexposure + 1,
                        nexposures);
          return NULL;
        }
      } else {
        ndetections = cpl_table_get_nrow(detections);
      }

      if ((ndetections > 0) &&
          ((ndetections >= nobjects[0]) && (ndetections <= nobjects[1]))) {
        iterate = CPL_FALSE;
      } else {
        cpl_table_delete(detections);
        detections = NULL;
        if (++iteration < aParams->iterations) {
          if (ndetections > nobjects[1]) {
            threshold += aParams->step;
          } else {
            threshold -= aParams->step;
            if (threshold <= 0.) {
              iterate = CPL_FALSE;
            }
          }
        } else {
          iterate = CPL_FALSE;
        }
      }
    } /* while iterate */
    cpl_image_delete(image);

    if (!detections) {
      if (ndetections == 0) {
        cpl_msg_warning(__func__, "No point-sources were detected in image %"
                        CPL_SIZE_FORMAT, iexposure + 1);
      } else if (ndetections < nobjects[0]) {
        cpl_msg_warning(__func__, "Too few (%" CPL_SIZE_FORMAT ") point-sources "
                        "were detected in image %" CPL_SIZE_FORMAT, ndetections,
                        iexposure + 1);
      } else if (ndetections > nobjects[1]) {
        cpl_msg_warning(__func__, "Too many (%" CPL_SIZE_FORMAT ") point-sources "
                        "were detected in image %" CPL_SIZE_FORMAT, ndetections,
                        iexposure + 1);
      } else {
        cpl_msg_warning(__func__, "Invalid number (%" CPL_SIZE_FORMAT ") of "
                        "point-sources were detected in image %" CPL_SIZE_FORMAT,
                        ndetections, iexposure + 1);
      }
      srclists[iexposure]->table = muse_cpltable_new(muse_source_list_def, 0);
    } else {
      cpl_msg_info(__func__, "%" CPL_SIZE_FORMAT " point-sources detected in "
                   "image %" CPL_SIZE_FORMAT, ndetections, iexposure + 1);

      /* Compute right ascension and declination from the pixel coordinates of *
       * each detected point-source using the WCS information of each exposure */
      if (muse_align_celestial_from_pixel(detections, fov->header)) {
        muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
        cpl_msg_error(__func__, "Computing WCS coordinates of detected "
                      "sources from pixel positions failed for image %"
                      CPL_SIZE_FORMAT " of %" CPL_SIZE_FORMAT, iexposure + 1,
                      nexposures);
        return NULL;
      }

      cpl_size nsources = cpl_table_get_nrow(detections);
      cpl_table *srclist = muse_cpltable_new(muse_source_list_def, nsources);

      cpl_size isource;
      for (isource = 0; isource < nsources; ++isource) {
        double value = 0.;
        int invalid = 0;

        cpl_table_set_int(srclist, MUSE_SRCLIST_ID, isource, isource + 1);

        value = cpl_table_get_double(detections, MUSE_SRCLIST_X,
                                     isource, &invalid);
        cpl_table_set_double(srclist, MUSE_SRCLIST_X, isource, value);
        value = cpl_table_get_double(detections, MUSE_SRCLIST_Y,
                                     isource, &invalid);
        cpl_table_set_double(srclist, MUSE_SRCLIST_Y, isource, value);
        value = cpl_table_get_double(detections, MUSE_SRCLIST_FLUX,
                                     isource, &invalid);
        cpl_table_set_double(srclist, MUSE_SRCLIST_FLUX, isource, value);
        value = cpl_table_get_double(detections, MUSE_SRCLIST_SHARPNESS,
                                     isource, &invalid);
        cpl_table_set_double(srclist, MUSE_SRCLIST_SHARPNESS, isource, value);
        value = cpl_table_get_double(detections, MUSE_SRCLIST_ROUNDNESS,
                                     isource, &invalid);
        cpl_table_set_double(srclist, MUSE_SRCLIST_ROUNDNESS, isource, value);
        value = cpl_table_get_double(detections, MUSE_SRCLIST_RA,
                                     isource, &invalid);
        cpl_table_set_double(srclist, MUSE_SRCLIST_RA, isource, value);
        cpl_table_set_double(srclist, MUSE_SRCLIST_RACORR, isource, value);
        value = cpl_table_get_double(detections, MUSE_SRCLIST_DEC,
                                     isource, &invalid);
        cpl_table_set_double(srclist, MUSE_SRCLIST_DEC, isource, value);
        cpl_table_set_double(srclist, MUSE_SRCLIST_DECCORR, isource, value);
      }

      srclists[iexposure]->table = srclist;
      cpl_table_delete(detections);
    }

    srclists[iexposure]->header = cpl_propertylist_duplicate(fov->header);

    cpl_propertylist_erase_regexp(srclists[iexposure]->header,
                                  "^(BUNIT|ESO QC.*)", CPL_FALSE);

    cpl_propertylist_update_string(srclists[iexposure]->header,
                                   QC_ALIGN_SRC_POSITIONS, "detection");
    cpl_propertylist_update_int(srclists[iexposure]->header,
                                QC_ALIGN_NDET, ndetections);
    cpl_propertylist_update_float(srclists[iexposure]->header,
                                  QC_ALIGN_BKG_MEDIAN, bkg);
    cpl_propertylist_update_float(srclists[iexposure]->header,
                                  QC_ALIGN_BKG_MAD, bkg_sigma);
    cpl_propertylist_update_float(srclists[iexposure]->header,
                                  QC_ALIGN_THRESHOLD, threshold);

  } /* for iexposure */

  return srclists;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Compute distance between object positions from position differences.
  @param    aDeltaRA   Matrix of right ascension offsets.
  @param    aDeltaDEC  Matrix of declination offsets.
  @param    aMeanDEC   Matrix of mean declination.
  @param    aOffsetRA  Optional, additional right ascension offset.
  @param    aOffsetDEC Optional, additional declination offset.
  @return   A matrix containing the computed distances, or NULL.

  The function computes for each matrix element the angular distance from
  the given position and differences in right ascension and declination.
  Extra shift parameters may be given. They are completely ignored if they are
  set to NULL, otherwise they are applied as extra offsets in the computation.
 */
/*----------------------------------------------------------------------------*/
static cpl_matrix *
muse_align_compute_distances(const cpl_matrix *aDeltaRA, const cpl_matrix *aDeltaDEC,
                             const cpl_matrix *aMeanDEC, const double *aOffsetRA,
                             const double *aOffsetDEC)
{

  cpl_errorstate status = cpl_errorstate_get();

  /* XXX: Cannot use cpl_matrix_power() here, since it chokes on negative *
   *      elements, however it should not if the exponent is integer!     */

  cpl_matrix *dDEC;
  if (aOffsetDEC) {
    cpl_matrix *tmp = cpl_matrix_duplicate(aDeltaDEC);
    cpl_matrix_subtract_scalar(tmp, *aOffsetDEC);

    dDEC = muse_cplmatrix_multiply_create(tmp, tmp);
    cpl_matrix_delete(tmp);
  } else {
    dDEC = muse_cplmatrix_multiply_create(aDeltaDEC, aDeltaDEC);
  }

  cpl_matrix *dec = cpl_matrix_duplicate(aMeanDEC);
  muse_cplmatrix_cosine(dec);

  cpl_matrix *dRA;
  if (aOffsetRA) {
    dRA = cpl_matrix_duplicate(aDeltaRA);
    cpl_matrix_subtract_scalar(dRA, *aOffsetRA);
    cpl_matrix_multiply(dRA, dec);
  } else {
    dRA = muse_cplmatrix_multiply_create(aDeltaRA, dec);
  }
  cpl_matrix_delete(dec);

  cpl_matrix *distance = muse_cplmatrix_multiply_create(dRA, dRA);
  cpl_matrix_delete(dRA);

  cpl_matrix_add(distance, dDEC);
  cpl_matrix_power(distance, 0.5);
  cpl_matrix_delete(dDEC);

  if (status != cpl_errorstate_get()) {
    cpl_matrix_delete(distance);
    return NULL;
  }

  return distance;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Estimate field offsets from a set of object position offsets
  @param    aOffsetRA  Estimated right ascension offset of the fields.
  @param    aOffsetDEC Estimated declination offset of the fields.
  @param    aWeight    Weight of the estimated offset (number of occurrences
                       of the estimates offset).
  @param    aDeltaRA   Right ascension offset matrix of pairs of detections
  @param    aDeltaDEC  Declination offset matrix of pairs of detections
  @param    aMeanDEC   Mean declination of pairs of detections
  @param    aRadius    Maximum allowed distance between candidate sources
  @param    aNbins     Number of offset histogram bins used to estimate the
                       field offset.
  @return   the number of matches or zero on failure to find matches

  Uses a histogram, extending from -aRadius to aRadius on both axes, to
  estimate the field offset as the most likely offset (histogram peak) of
  object positions.
 */
/*----------------------------------------------------------------------------*/
static cpl_size
muse_align_estimate_offsets(double *aOffsetRA, double *aOffsetDEC, double *aWeight,
                            const cpl_matrix *aDeltaRA, const cpl_matrix *aDeltaDEC,
                            const cpl_matrix *aMeanDEC, double aRadius, int aNbins)
{
  cpl_ensure((aOffsetRA && aOffsetDEC) && aWeight, CPL_ERROR_NULL_INPUT, 0);
  cpl_ensure((aDeltaRA && aDeltaDEC) && aMeanDEC, CPL_ERROR_NULL_INPUT, 0);
  cpl_ensure(cpl_matrix_get_nrow(aDeltaRA) == cpl_matrix_get_nrow(aDeltaDEC),
             CPL_ERROR_INCOMPATIBLE_INPUT, 0);
  cpl_ensure(cpl_matrix_get_nrow(aDeltaRA) == cpl_matrix_get_nrow(aMeanDEC),
             CPL_ERROR_INCOMPATIBLE_INPUT, 0);
  cpl_ensure(aRadius > 0., CPL_ERROR_ILLEGAL_INPUT, 0);
  cpl_ensure(aNbins > 0, CPL_ERROR_ILLEGAL_INPUT, 0);

  cpl_matrix *distance = muse_align_compute_distances(aDeltaRA, aDeltaDEC,
                                                      aMeanDEC, NULL, NULL);
  if (cpl_matrix_get_min(distance) >= aRadius) {
    cpl_matrix_delete(distance);
    *aOffsetRA  = 0.;
    *aOffsetDEC = 0.;
    *aWeight    = 1.;
    return 0;
  }

  cpl_array *selection = muse_cplmatrix_where(distance, aRadius,
                                              _muse_condition_less_than);
  cpl_matrix_delete(distance);
  cpl_matrix *dRA  = muse_cplmatrix_extract_selected(aDeltaRA, selection);
  cpl_matrix *dDEC = muse_cplmatrix_extract_selected(aDeltaDEC, selection);
  cpl_array_delete(selection);

  /* Search box from -aRadius to aRadius for both axes. */
  double bstep = 2. * aRadius / (double)aNbins;

  /* Note: Despite of the previous selection of positions which have        *
   * distances smaller than the search radius the bin index of the right    *
   * ascension may become invalid and must be restricted to valid range.    *
   * The reason is that in the following the difference in right ascension  *
   * is directly compared to the search radius, while in the selection of   *
   * positions this difference is corrected by the cosine of the mean       *
   * declination (cf. muse_align_compute_distances()), and thus differences *
   * in right ascension which are larger than the search radius may still   *
   * be accepted by the distance criterion!                                 *
   * To play it save also the index along the y-axis is validated.          */

  cpl_matrix *bins = cpl_matrix_new(1, aNbins);
  cpl_size ibin;
  for (ibin = 0; ibin < aNbins; ++ibin) {
    cpl_matrix_set(bins, 0, ibin, -aRadius + ibin * bstep);
  }
  cpl_matrix *histogram = cpl_matrix_new(aNbins, aNbins);
  double *hdata = cpl_matrix_get_data(histogram);
  cpl_size npos = cpl_matrix_get_ncol(dRA);
  cpl_size nmatch = 0;
  cpl_size ipos;
  for (ipos = 0; ipos < npos; ++ipos) {
    cpl_size ix = (cpl_size)((cpl_matrix_get(dRA, 0, ipos) + aRadius) / bstep);
    cpl_size iy = (cpl_size)((cpl_matrix_get(dDEC, 0, ipos) + aRadius) / bstep);
    if (((ix >= 0) && (ix < aNbins)) && ((iy >= 0) && (iy < aNbins))) {
      hdata[iy * aNbins + ix] += 1.;
      ++nmatch;
    }
  }
  cpl_matrix_delete(dRA);
  cpl_matrix_delete(dDEC);

  if (nmatch == 0) {
    cpl_matrix_delete(histogram);
    cpl_matrix_delete(bins);
    return 0;
  }

  cpl_size row;
  cpl_size column;
  cpl_matrix_get_maxpos(histogram, &row, &column);

  *aOffsetRA  = cpl_matrix_get(bins, 0, column) + 0.5 * bstep;
  *aOffsetDEC = cpl_matrix_get(bins, 0, row) + 0.5 * bstep;
  *aWeight    = cpl_matrix_get_max(histogram);
  cpl_matrix_delete(histogram);
  cpl_matrix_delete(bins);

  return nmatch;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Updates an estimated field offset from a set of object position
            offsets
  @param    aOffsetRA  Initial estimate of the right ascension offset of the
                       fields.
  @param    aOffsetDEC Initial estimate of the declination offset of the fields.
  @param    aWeight    Weight of the estimated offset (number of occurrences
                       of the estimates offset).
  @param    aDeltaRA   Right ascension offset matrix of pairs of detections
  @param    aDeltaDEC  Declination offset matrix of pairs of detections
  @param    aMeanDEC   Mean declination of pairs of detections
  @param    aRadius    Maximum allowed distance between candidate sources
  @return   the number of matches or zero on failure to find matches

  The initially given estimate of the field offset is updated (refined) to be
  the median of the offset of the object positions after correcting for the
  initial estimate.
 */
/*----------------------------------------------------------------------------*/
static cpl_size
muse_align_update_offsets(double *aOffsetRA, double *aOffsetDEC, double *aWeight,
                          const cpl_matrix *aDeltaRA, const cpl_matrix *aDeltaDEC,
                          const cpl_matrix *aMeanDEC, double aRadius)
{
  cpl_ensure((aOffsetRA && aOffsetDEC) && aWeight, CPL_ERROR_NULL_INPUT, 0);
  cpl_ensure((aDeltaRA && aDeltaDEC) && aMeanDEC, CPL_ERROR_NULL_INPUT, 0);
  cpl_ensure(cpl_matrix_get_nrow(aDeltaRA) == cpl_matrix_get_nrow(aDeltaDEC),
             CPL_ERROR_INCOMPATIBLE_INPUT, 0);
  cpl_ensure(cpl_matrix_get_nrow(aDeltaRA) == cpl_matrix_get_nrow(aMeanDEC),
             CPL_ERROR_INCOMPATIBLE_INPUT, 0);
  cpl_ensure(aRadius > 0., CPL_ERROR_ILLEGAL_INPUT, 0);

  cpl_matrix *distance = muse_align_compute_distances(aDeltaRA, aDeltaDEC,
                                                      aMeanDEC, aOffsetRA, aOffsetDEC);
  if (cpl_matrix_get_min(distance) >= aRadius) {
    cpl_matrix_delete(distance);
    *aOffsetRA  = 0.;
    *aOffsetDEC = 0.;
    *aWeight    = 1.;
    return 0;
  }

  cpl_array *selection = muse_cplmatrix_where(distance, aRadius,
                                              _muse_condition_less_than);
  cpl_matrix_delete(distance);
  cpl_matrix *dRA  = muse_cplmatrix_extract_selected(aDeltaRA, selection);
  cpl_matrix *dDEC = muse_cplmatrix_extract_selected(aDeltaDEC, selection);
  cpl_array_delete(selection);

  cpl_size nselected = cpl_matrix_get_ncol(dRA);
  double variance = 2. * aRadius * aRadius;
  if (nselected > 1) {
    double sdevRA  = cpl_matrix_get_stdev(dRA);
    double sdevDEC = cpl_matrix_get_stdev(dDEC);
    double _variance = sdevRA * sdevRA + sdevDEC * sdevDEC;
    if (_variance > nselected / DBL_MAX) {
      variance = _variance;
    }
  }

  *aOffsetRA  = cpl_matrix_get_median(dRA);
  *aOffsetDEC = cpl_matrix_get_median(dDEC);
  *aWeight = nselected / variance;

  cpl_matrix_delete(dRA);
  cpl_matrix_delete(dDEC);

  return nselected;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Compute field offsets for a set of exposures
  @param    aImagelist     the list of FOV images
  @param    aCataloglist   the list of sources for each FOV image.
  @param    aFieldId       list of field identifiers.
  @param    aSearchradius  list of search radii
  @param    aNbins         number of histogram bins for first iteration
  @param    aWeight        Turn weighting on and off
  @param    aHeader        FITS header to add QC parameters
  @return   an offset table on success, or NULL otherwise.

  @note Passing a NULL propertylist is not a critical failure but results in
        lots of error messages about that.
 */
/*----------------------------------------------------------------------------*/
static cpl_table *
muse_align_compute_field_offsets(muse_imagelist *aImagelist,
                                 muse_table **aCataloglist,
                                 const cpl_array *aFieldId,
                                 const cpl_array *aSearchradius,
                                 int aNbins, cpl_boolean aWeight,
                                 cpl_propertylist *aHeader)
{
  cpl_ensure(aImagelist && aCataloglist && aFieldId,
             CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(aSearchradius, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(cpl_array_get_size(aFieldId) == muse_imagelist_get_size(aImagelist),
             CPL_ERROR_ILLEGAL_INPUT, NULL);
  cpl_ensure(aNbins > 1, CPL_ERROR_ILLEGAL_INPUT, NULL);

  cpl_size nfields = muse_imagelist_get_size(aImagelist);
  cpl_ensure(nfields >= 2, CPL_ERROR_DATA_NOT_FOUND, NULL);

  /* Create a list of all possible pairings of the input fields *
   * number of combinations: nfields choose 2                   */
  cpl_size npairs = nfields * (nfields - 1) / 2;

  muse_indexpair *combinations = cpl_calloc(npairs, sizeof *combinations);
  cpl_size ipair = 0;
  cpl_size ifield;
  for (ifield = 0; ifield < nfields; ++ifield) {
    cpl_size jfield;
    for (jfield = ifield + 1; jfield < nfields; ++jfield) {
      combinations[ipair].first = ifield;
      combinations[ipair].second = jfield;
      ++ipair;
    }
  }

  /* record number of detections in each image as QC parameter */
  for (ifield = 0; ifield < nfields; ++ifield) {
    cpl_size kfield = cpl_array_get_cplsize(aFieldId, ifield, NULL);
    char *kw = cpl_sprintf(QC_ALIGN_NDETi, (int)kfield + 1);
    cpl_propertylist_append_int(aHeader, kw,
                                cpl_table_get_nrow(aCataloglist[ifield]->table));
    cpl_free(kw);
  }

  /* prepare the data structure to record the number of matching  *
   * objects found in each of the other fields to, eventually,    *
   * compute the median number of matching detections for each    *
   * field.                                                       */
  cpl_array **aoverlaps = cpl_calloc(nfields, sizeof(cpl_array *));
  for (ifield = 0; ifield < nfields; ++ifield) {
    aoverlaps[ifield] = cpl_array_new(nfields, CPL_TYPE_INT);
  }

  /* search for correlations */
  cpl_matrix *ra_offsets = NULL;
  cpl_matrix *dec_offsets = NULL;
  cpl_size *has_neighbors = cpl_malloc(nfields * sizeof *has_neighbors);
  cpl_size isearch;
  for (isearch = 0; isearch < cpl_array_get_size(aSearchradius); ++isearch) {
    double radius = cpl_array_get_double(aSearchradius, isearch, NULL);

    /* Clear the number of matching detections. Note that this resets all   *
     * elements to valid. To exclude the elements which should not be taken *
     * into account in the final median computation they are invalidated.   *
     * This must at least be done for the "diagonal" elements of this       *
     * matrix like structure which would refer to the number of matching    *
     * detections a field has with itself, since these are never computed.  */
    for (ifield = 0; ifield < nfields; ++ifield) {
      cpl_array_fill_window_int(aoverlaps[ifield], 0, nfields, 0);
      /* XXX: To include combinations with zero matches in the median *
       *      statistic, use the alternative invalidation scheme      */
#if 0
      cpl_array_set_invalid(aoverlaps[ifield], ifield);
#else
      cpl_array_fill_window_invalid(aoverlaps[ifield], 0, nfields);
#endif
    }

    /* The linear system consists of one row for each pair of fields, and *
     * the closure relation                                               */
    cpl_matrix *weights = cpl_matrix_new(npairs + 1, nfields);
    cpl_matrix *offsets_ra = cpl_matrix_new(npairs + 1, 1);
    cpl_matrix *offsets_dec = cpl_matrix_new(npairs + 1, 1);

    cpl_matrix_fill_row(weights, 1., 0);
    cpl_size kpairs = 0;

    memset(has_neighbors, 0, nfields * sizeof *has_neighbors);

    for (ipair = 0; ipair < npairs; ++ipair) {
      /* Compute the distance between any 2 stars in the 2 fields */
      const cpl_table *catalog1 = aCataloglist[combinations[ipair].first]->table;
      const cpl_table *catalog2 = aCataloglist[combinations[ipair].second]->table;

      cpl_size nstars1 = cpl_table_get_nrow(catalog1);
      cpl_size nstars2 = cpl_table_get_nrow(catalog2);

      const double *ra1 = cpl_table_get_data_double_const(catalog1, "RA");
      const double *ra2 = cpl_table_get_data_double_const(catalog2, "RA");
      const double *dec1 = cpl_table_get_data_double_const(catalog1, "DEC");
      const double *dec2 = cpl_table_get_data_double_const(catalog2, "DEC");

      cpl_matrix *delta_ra  = cpl_matrix_new(nstars1, nstars2);
      cpl_matrix *delta_dec = cpl_matrix_new(nstars1, nstars2);
      cpl_matrix *dec_mean  = cpl_matrix_new(nstars1, nstars2);

      cpl_size irow;
      for (irow = 0; irow < nstars1; ++irow) {
        cpl_size icol;
        for (icol = 0; icol < nstars2; ++icol) {
          cpl_matrix_set(delta_ra, irow, icol, (ra1[irow] - ra2[icol]) * deg2as);
          cpl_matrix_set(delta_dec, irow, icol, (dec1[irow] - dec2[icol]) * deg2as);
          cpl_matrix_set(dec_mean, irow, icol,
                         0.5 *(dec1[irow] + dec2[icol]) * CPL_MATH_RAD_DEG);
        }
      }

      double offset_ra = 0.;
      double offset_dec = 0.;
      double weight = 1.;
      cpl_size noverlap = 0;
      if (isearch == 0) {
        noverlap = muse_align_estimate_offsets(&offset_ra, &offset_dec, &weight,
                                               delta_ra, delta_dec, dec_mean,
                                               radius, aNbins);
      } else {
        offset_ra = cpl_matrix_get(ra_offsets, combinations[ipair].first, 0) -
            cpl_matrix_get(ra_offsets, combinations[ipair].second, 0);
        offset_dec = cpl_matrix_get(dec_offsets, combinations[ipair].first, 0) -
            cpl_matrix_get(dec_offsets, combinations[ipair].second, 0);

        noverlap = muse_align_update_offsets(&offset_ra, &offset_dec, &weight,
                                             delta_ra, delta_dec, dec_mean,
                                             radius);
      }
      cpl_matrix_delete(dec_mean);
      cpl_matrix_delete(delta_ra);
      cpl_matrix_delete(delta_dec);

      if (noverlap > 0) {
        if (!aWeight) {
          weight = 1.;
        }

        ++kpairs;
        cpl_matrix_set(offsets_ra, kpairs, 0, weight * offset_ra);
        cpl_matrix_set(offsets_dec, kpairs, 0, weight * offset_dec);
        cpl_matrix_set(weights, kpairs, combinations[ipair].first, weight);
        cpl_matrix_set(weights, kpairs, combinations[ipair].second, -weight);
        has_neighbors[combinations[ipair].first] += 1;
        has_neighbors[combinations[ipair].second] += 1;

        /* record the number of overlaps for later median computation */
        cpl_array_set_int(aoverlaps[combinations[ipair].first],
                          combinations[ipair].second, noverlap);
        cpl_array_set_int(aoverlaps[combinations[ipair].second],
                          combinations[ipair].first, noverlap);
      } /* if noverlap positive */
    } /* for ipair */

    /* Replace the solution from the previous iteration */
    cpl_matrix_delete(ra_offsets);
    cpl_matrix_delete(dec_offsets);

    /* If there is no field with overlapping neighbors there is no  *
     * correction to the field coordinates, i.e. all offsets are 0. */
    if (!kpairs) {
      cpl_matrix_delete(offsets_dec);
      cpl_matrix_delete(offsets_ra);
      cpl_matrix_delete(weights);

      cpl_msg_warning(__func__, "No overlapping fields of view were found "
                      "for search radius %.4f!", radius);

      ra_offsets = cpl_matrix_new(nfields, 1);
      dec_offsets = cpl_matrix_new(nfields, 1);

    } else {
      /* Use only the valid part of the matrices when solving the linear system */
      cpl_matrix *_weights     = cpl_matrix_wrap(kpairs + 1, nfields,
                                                 cpl_matrix_get_data(weights));
      cpl_matrix *_offsets_ra  = cpl_matrix_wrap(kpairs + 1, 1,
                                                 cpl_matrix_get_data(offsets_ra));
      cpl_matrix *_offsets_dec = cpl_matrix_wrap(kpairs + 1, 1,
                                                 cpl_matrix_get_data(offsets_dec));

      ra_offsets = muse_cplmatrix_solve_least_square(_weights, _offsets_ra);
      dec_offsets = muse_cplmatrix_solve_least_square(_weights, _offsets_dec);

      cpl_matrix_unwrap(_offsets_dec);
      cpl_matrix_unwrap(_offsets_ra);
      cpl_matrix_unwrap(_weights);

      cpl_matrix_delete(offsets_dec);
      cpl_matrix_delete(offsets_ra);
      cpl_matrix_delete(weights);
    }
  } /* for isearch */
  cpl_free(combinations);

  /* Build the results table from the matrices, and add observation start  *
   * from the image header as observation identifier. For isolated fields, *
   * i.e.fields which do not overlap with any other, the pointing          *
   * corrections are forced to 0. For fields which have an offset could be *
   * computed it is converted to degrees.                                  */
  cpl_table *offsets = muse_cpltable_new(muse_offset_list_def,
                                         muse_imagelist_get_size(aImagelist));
  cpl_size minmatch = LLONG_MAX,
           nomatch = 0;
  for (ifield = 0; ifield < nfields; ++ifield) {
    muse_image *fov = muse_imagelist_get(aImagelist, ifield);
    cpl_size field_id = cpl_array_get_cplsize(aFieldId, ifield, NULL);
    const char *timestamp = muse_pfits_get_dateobs(fov->header);
    double mjd = muse_pfits_get_mjdobs(fov->header);
    cpl_table_set_string(offsets, MUSE_OFFSETS_DATEOBS, ifield, timestamp);
    cpl_table_set_double(offsets, MUSE_OFFSETS_MJDOBS, ifield, mjd);

    if (has_neighbors[ifield]) {
      double ra_offset = cpl_matrix_get(ra_offsets, ifield, 0);
      double dec_offset = cpl_matrix_get(dec_offsets, ifield, 0);
      cpl_table_set_double(offsets, MUSE_OFFSETS_DRA, ifield, ra_offset / deg2as);
      cpl_table_set_double(offsets, MUSE_OFFSETS_DDEC, ifield, dec_offset / deg2as);
      /* record QC parameter */
      int nmedian = cpl_array_get_median(aoverlaps[ifield]);
      char *kw = cpl_sprintf(QC_ALIGN_NMATCHi, (int)field_id + 1);
      cpl_propertylist_append_int(aHeader, kw, nmedian);
      cpl_free(kw);
      if (nmedian < minmatch) {
        minmatch = nmedian;
      }
    } else {
      cpl_table_set_double(offsets, MUSE_OFFSETS_DRA, ifield, 0.);
      cpl_table_set_double(offsets, MUSE_OFFSETS_DDEC, ifield, 0.);
      char *kw = cpl_sprintf(QC_ALIGN_NMATCHi, (int)field_id + 1);
      cpl_propertylist_append_int(aHeader, kw, 0);
      cpl_free(kw);
      nomatch++;
    }
  } /* for ifield */
  cpl_propertylist_append_int(aHeader, QC_ALIGN_NMATCH_MIN, minmatch);
  cpl_propertylist_append_int(aHeader, QC_ALIGN_NOMATCH, nomatch);
  muse_vfree((void **)aoverlaps, nfields, (muse_free_func)cpl_array_delete);
  cpl_free(has_neighbors);
  cpl_matrix_delete(dec_offsets);
  cpl_matrix_delete(ra_offsets);

  /* Force the offset of the first field of view to be zero and *
   * invalidate the flux scaling column, so that by no scaling  *
   * occurs during the exposure combination.                    */
  double ra_shift  = cpl_table_get(offsets, MUSE_OFFSETS_DRA, 0, NULL);
  double dec_shift = cpl_table_get(offsets, MUSE_OFFSETS_DDEC, 0, NULL);
  cpl_table_subtract_scalar(offsets, MUSE_OFFSETS_DRA, ra_shift);
  cpl_table_subtract_scalar(offsets, MUSE_OFFSETS_DDEC, dec_shift);
  cpl_table_set_column_invalid(offsets, MUSE_OFFSETS_FSCALE, 0, nfields);

  /* Extra QC parameters: offset statistics, in arcsec */
  double qcmin  = cpl_table_get_column_min(offsets, MUSE_OFFSETS_DRA) * 3600.;
  double qcmax  = cpl_table_get_column_max(offsets, MUSE_OFFSETS_DRA) * 3600.;
  double qcmean = cpl_table_get_column_mean(offsets, MUSE_OFFSETS_DRA) * 3600.;
  double qcsdev = cpl_table_get_column_stdev(offsets, MUSE_OFFSETS_DRA) * 3600.;

  cpl_propertylist_append_float(aHeader, QC_ALIGN_DRA_MIN, qcmin);
  cpl_propertylist_append_float(aHeader, QC_ALIGN_DRA_MAX, qcmax);
  cpl_propertylist_append_float(aHeader, QC_ALIGN_DRA_MEAN, qcmean);
  cpl_propertylist_append_float(aHeader, QC_ALIGN_DRA_STDEV, qcsdev);

  qcmin  = cpl_table_get_column_min(offsets, MUSE_OFFSETS_DDEC) * 3600.;
  qcmax  = cpl_table_get_column_max(offsets, MUSE_OFFSETS_DDEC) * 3600.;
  qcmean = cpl_table_get_column_mean(offsets, MUSE_OFFSETS_DDEC) * 3600.;
  qcsdev = cpl_table_get_column_stdev(offsets, MUSE_OFFSETS_DDEC) * 3600.;

  cpl_propertylist_append_float(aHeader, QC_ALIGN_DDEC_MIN, qcmin);
  cpl_propertylist_append_float(aHeader, QC_ALIGN_DDEC_MAX, qcmax);
  cpl_propertylist_append_float(aHeader, QC_ALIGN_DDEC_MEAN, qcmean);
  cpl_propertylist_append_float(aHeader, QC_ALIGN_DDEC_STDEV, qcsdev);

  return offsets;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Create a list of exposure maps from a list of FOV images.
  @param    aImagelist     the list of FOV images
  @return   an new image list containing the exposure map of each input FOV
            image, or NULL if an error occurred.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aImagelist is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         The input image list is empty}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         The property EXPTIME is missing}

  For each image in aImagelist create a duplicate where each pixel is filled
  with the exposure time taken from the EXPTIME keyword of the FOV image
  header. If the input image has a bad pixel mask, the mask is propagated
  to the created exposure map.
 */
/*----------------------------------------------------------------------------*/
static muse_imagelist *
muse_align_create_expmaps(muse_imagelist *aImagelist)
{
  cpl_ensure(aImagelist, CPL_ERROR_NULL_INPUT, NULL);

  cpl_size nexposures = muse_imagelist_get_size(aImagelist);
  cpl_ensure(nexposures > 0, CPL_ERROR_ILLEGAL_INPUT, NULL);

  muse_imagelist *expmaps = muse_imagelist_new();
  cpl_size iexposure;
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    muse_image *_fov = muse_imagelist_get(aImagelist, iexposure);
    if (cpl_propertylist_has(_fov->header, "EXPTIME")) {
      double exptime = muse_pfits_get_exptime(_fov->header);
      if (exptime < 0.) {
        cpl_msg_warning(__func__, "Invalid exposure time %.3f in FOV image %"
                      CPL_SIZE_FORMAT ". Using zero exposure time!", exptime,
                      iexposure + 1);
        exptime = 0.;
      }

      muse_image *_expmap = muse_image_new();
      _expmap->data = cpl_image_duplicate(_fov->data);
      cpl_mask *bpm = cpl_image_unset_bpm(_expmap->data);

      /* XXX: For debugging purposes only! Remove for final release! *
       *      Saving a preview, combined field-of-view image may be  *
       *      added at a later stage!                                */
      if (!getenv("MUSE_DEBUG_EXPMAP")) {
        cpl_size nx = cpl_image_get_size_x(_fov->data);
        cpl_size ny = cpl_image_get_size_y(_fov->data);
        cpl_image_fill_window(_expmap->data, 1, 1, nx, ny, exptime);
      }
      cpl_image_set_bpm(_expmap->data, bpm);
      cpl_image_fill_rejected(_expmap->data, 0.);
      _expmap->header = cpl_propertylist_duplicate(_fov->header);
      cpl_propertylist_update_string(_expmap->header, "BUNIT", "s");

      muse_imagelist_set(expmaps, _expmap, iexposure);
    } else {
      cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND,
                            "Missing \"EXPTIME\" in FOV image %"
                            CPL_SIZE_FORMAT, iexposure + 1);
      muse_imagelist_delete(expmaps);
      expmaps = NULL;
      break;
    }
  }
  return expmaps;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Correct the WCS of each image in the input list for the given offsets.
  @param    aImagelist    the list of FOV images
  @param    aOffsets      the table with the coordinate offsets to apply
  @return   CPL_ERROR_NONE on success or an appropriate error code on failure.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         One or more arguments are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         The offset table does not provide the coordinate offset columns}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         The size of aImagelist does not match the number of rows in aOffsets}

  For each image in the input list the coordinate values of the reference
  pixel of the WCS is corrected for the corresponding offset in the given
  offset table.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_align_correct_offsets(muse_imagelist *aImagelist, cpl_table *aOffsets)
{
  cpl_ensure(aImagelist && aOffsets, CPL_ERROR_NULL_INPUT,
             CPL_ERROR_NULL_INPUT);
  cpl_ensure((cpl_table_has_column(aOffsets, MUSE_OFFSETS_DRA) &&
             cpl_table_has_column(aOffsets, MUSE_OFFSETS_DDEC)),
             CPL_ERROR_ILLEGAL_INPUT, CPL_ERROR_ILLEGAL_INPUT);

  cpl_size nexposures = muse_imagelist_get_size(aImagelist);
  cpl_ensure(nexposures == cpl_table_get_nrow(aOffsets),
             CPL_ERROR_INCOMPATIBLE_INPUT, CPL_ERROR_INCOMPATIBLE_INPUT);

  cpl_size iexposure;
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    muse_image *image = muse_imagelist_get(aImagelist, iexposure);
    double ra = muse_pfits_get_crval(image->header, 1);
    double dec = muse_pfits_get_crval(image->header, 2);
    double shift_ra = cpl_table_get_double(aOffsets, MUSE_OFFSETS_DRA,
                                           iexposure, NULL);
    double shift_dec = cpl_table_get_double(aOffsets, MUSE_OFFSETS_DDEC,
                                           iexposure, NULL);
    if (isfinite(shift_ra) && isfinite(shift_dec)) {
      cpl_propertylist_update_double(image->header, "CRVAL1", ra - shift_ra);
      cpl_propertylist_update_double(image->header, "CRVAL2", dec - shift_dec);
    }
  }
  return CPL_ERROR_NONE;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Calculate the extent of the field-of-view of a FOV mosaic.
  @param    aImagelist    the list of FOV images to combine
  @param    aRA0          the minimum right ascension of the mosaic FOV
  @param    aDEC0         the minimum declination of the mosaic FOV
  @param    aRA1          the maximum right ascension of the mosaic FOV
  @param    aDEC1         the maximum declination of the mosaic FOV
  @return   CPL_ERROR_NONE on success or an appropriate error code on failure.

  @error{set CPL_ERROR_NULL_INPUT\, return CPL_ERROR_NULL_INPUT,
         One or more arguments are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return CPL_ERROR_ILLEGAL_INPUT,
         The target WCS is not a gnomonic WCS}

  From the images in the input list the largest and the smallest possible value
  of the right ascension and the declination in the individual FOV images is
  determined. The minimum and the maximum value of these coordinate values
  is used as the extent of the combined field-of-view of the mosaic.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_align_mosaic_get_fov(muse_imagelist *aImagelist,
                          double *aRA0, double *aDEC0,
                          double *aRA1, double *aDEC1)
{
  cpl_ensure(aImagelist, CPL_ERROR_NULL_INPUT, CPL_ERROR_NULL_INPUT);
  cpl_ensure((aRA0 && aDEC0) && (aRA1 && aDEC1),
             CPL_ERROR_NULL_INPUT, CPL_ERROR_NULL_INPUT);

  cpl_size nexposures = muse_imagelist_get_size(aImagelist);

  cpl_vector *ra = cpl_vector_new(4 * nexposures);
  cpl_vector *dec = cpl_vector_new(4 * nexposures);
  double *_ra = cpl_vector_get_data(ra);
  double *_dec = cpl_vector_get_data(dec);

  cpl_error_code status = CPL_ERROR_NONE;
  cpl_size iexposure;
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    muse_image *image = muse_imagelist_get(aImagelist, iexposure);
    if (muse_align_wcs_is_gnomonic(image->header)) {
      cpl_size _nx = cpl_image_get_size_x(image->data);
      cpl_size _ny = cpl_image_get_size_y(image->data);

      muse_wcs *wcs = muse_wcs_new(image->header);
      wcs->iscelsph = CPL_TRUE;
      cpl_size k = 4 * iexposure;
      muse_wcs_celestial_from_pixel_fast(wcs, 1, 1, &_ra[k], &_dec[k]);
      ++k;
      muse_wcs_celestial_from_pixel_fast(wcs, 1, _ny, &_ra[k], &_dec[k]);
      ++k;
      muse_wcs_celestial_from_pixel_fast(wcs, _nx, 1, &_ra[k], &_dec[k]);
      ++k;
      muse_wcs_celestial_from_pixel_fast(wcs, _nx, _ny, &_ra[k], &_dec[k]);
      cpl_free(wcs);
    } else {
      cpl_error_set_message(__func__, CPL_ERROR_UNSUPPORTED_MODE,
                            "Exposure %" CPL_SIZE_FORMAT " of %"
                            CPL_SIZE_FORMAT " does not have a gnomonic"
                            " world coordinate system!", iexposure + 1,
                            nexposures);
      status = CPL_ERROR_UNSUPPORTED_MODE;
      break;
    }
  }

  if (status == CPL_ERROR_NONE) {
    *aRA0 = cpl_vector_get_min(ra);
    *aRA1 = cpl_vector_get_max(ra);
    *aDEC0 = cpl_vector_get_min(dec);
    *aDEC1 = cpl_vector_get_max(dec);
  }

  cpl_vector_delete(ra);
  cpl_vector_delete(dec);

  return status;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Create the target WCS of the FOV mosaic from a header containing
            WCS information and the given field-of-view limits.
  @param    aHeader       the header with the reference WCS
  @param    aRA0          the minimum right ascension of the mosaic FOV
  @param    aDEC0         the minimum declination of the mosaic FOV
  @param    aRA1          the maximum right ascension of the mosaic FOV
  @param    aDEC1         the maximum declination of the mosaic FOV
  @param    nx            the size of the FOV mosaic in pixels along the x-axis
  @param    ny            the size of the FOV mosaic in pixels along the y-axis
  @return   The created target WCS object on success or NULL on failure.

  @error{set CPL_ERROR_NULL_INPUT\, return CPL_ERROR_NULL_INPUT,
         One or more arguments are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return CPL_ERROR_ILLEGAL_INPUT,
         The target WCS is not a gnomonic WCS}

  From the given WCS and the extent of the given field-of-view, the size
  in pixel of an image is calculated which is needed to store the entire
  FOV region. The target WCS of the mosaic is created from the WCS
  information contained in the given header, and its origin is set to the
  field center.
 */
/*----------------------------------------------------------------------------*/
static muse_wcs *
muse_align_mosaic_setup_wcs(cpl_propertylist *aHeader,
                            double aRA0, double aDEC0,
                            double aRA1, double aDEC1,
                            double *aNX, double *aNY)
{
  cpl_ensure(aHeader, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(muse_align_wcs_is_gnomonic(aHeader), CPL_ERROR_ILLEGAL_INPUT,
             NULL);

  muse_wcs *wcs = muse_wcs_new(aHeader);
  wcs->iscelsph = CPL_TRUE;

  /* Convert to radians for the calculation of the field *
   * limits in pixel space. This is undone when the wcs *
   * origin is updated to be in the image center in the  *
   * following.                                         */
  wcs->crval1 *= CPL_MATH_RAD_DEG;
  wcs->crval2 *= CPL_MATH_RAD_DEG;

  double x[2];
  double y[2];
  muse_wcs_pixel_from_celestial_fast(wcs, aRA0 * CPL_MATH_RAD_DEG,
                                     aDEC0 * CPL_MATH_RAD_DEG, &x[0], &y[0]);
  muse_wcs_pixel_from_celestial_fast(wcs, aRA1 * CPL_MATH_RAD_DEG,
                                     aDEC1 * CPL_MATH_RAD_DEG, &x[1], &y[1]);

  /* Number of pixels along the x and the y axis. */
  double nx = fabs(x[1] - x[0]);
  double ny = fabs(y[1] - y[0]);

  /* Set origin of the world coordinate system to the field center. */
  wcs->crpix1 = 0.5 * nx;
  wcs->crpix2 = 0.5 * ny;
  wcs->crval1 = 0.5 * (aRA1 + aRA0);
  wcs->crval2 = 0.5 * (aDEC1 + aDEC0);

  *aNX = ceil(nx);
  *aNY = ceil(ny);
  return wcs;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Resample an image to the target WCS grid using a nearest neighbor
            interpolation and add the pixel values to the target image.
  @param    aTargetWCS    the target WCS to which the source image is resampled
  @param    aTargetImage  the target image to which the pixel values are added
  @param    aSourceImage  the source image
  @return

  @error{set CPL_ERROR_NULL_INPUT\, return CPL_ERROR_NULL_INPUT,
         One or more arguments are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return CPL_ERROR_ILLEGAL_INPUT,
         The target WCS or the source WCS is not a gnomonic WCS, or any
         of the data members of the source or the target image are
         non-compliant}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return CPL_ERROR_INCOMPATIBLE_INPUT,
         The source image data member @c dq is not @c NULL}

  For each pixel in the target image the corresponding sky coordinates, i.e.
  RA and DEC are determined. For this sky position its pixel coordinates in
  the source image are computed. The surrounding pixels are used to obtain
  the pixel coordinates of the nearest neighbor. The nearest neighbor is
  found by comparing the distance in sky coordinates of each of the
  surrounding pixels with the sky position of the current target pixel.
  Finally the pixel value of nearest neighbor found in the source image is
  added to the pixel value of the current target pixel.

  If a target pixel does not have a counterpart in the current source image,
  it is recorded in a mask of missing and set pixels, i.e. the pixel values
  are either @c EURO3D_MISSDATA or @c EURO3D_GOODPIXEL, respectively. This
  mask is eventually merged with the data quality information which may already
  be present in the target image by performing a logical AND operation. Thus,
  if the function is used to resample multiple input images onto the same
  target image, the final data quality image contains a mask of pixels, where
  good pixels have at least once been set and received a valid pixel value from
  one of the source images. This also means that on the first call of this
  function, the data quality member of the input image should be @c NULL.

  If the selected nearest neighbor pixel of the source image has been rejected,
  i.e. the associated bad pixel map of the @c data member of the source image
  is non-zero, the pixel value is not transferred to the target image and it
  is treated as missing data.

  The input source image must not provide a data quality image, i.e. the @c dq
  data member must be @c NULL. Instead, all 'bad' pixels should be flagged in
  the bad pixel mask of CPL image of the @c data data member. If the source
  image provides variance data, i.e. the @c stat data member is not @c NULL,
  it is transferred to the target image in the same way as the data values
  themselves, i.e. added to the target pixel value.

  The input target image must therefore provide image buffers for the data
  values and the variance information, i.e. the data members @c data and @stat
  of the target image @c aTargetImage must not be @c NULL. Thus, if not
  variance data is provided by the input source image, the @c stat data member
  of the target image remains unchanged on return.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_align_resampling_image_nearest_add(muse_wcs *aTargetWCS,
                                        muse_image *aTargetImage,
                                        muse_image *aSourceImage)
{
  cpl_ensure(aTargetWCS && aTargetImage && aSourceImage,
             CPL_ERROR_NULL_INPUT, CPL_ERROR_NULL_INPUT);
  cpl_ensure(aTargetWCS->iscelsph &&
             muse_align_wcs_is_gnomonic(aSourceImage->header),
             CPL_ERROR_ILLEGAL_INPUT, CPL_ERROR_ILLEGAL_INPUT);
  cpl_ensure(aTargetImage->data && aTargetImage->stat,
             CPL_ERROR_ILLEGAL_INPUT, CPL_ERROR_ILLEGAL_INPUT);
  cpl_ensure(aSourceImage->data && !aSourceImage->dq,
             CPL_ERROR_INCOMPATIBLE_INPUT, CPL_ERROR_INCOMPATIBLE_INPUT);

  cpl_size tnx = cpl_image_get_size_x(aTargetImage->data);
  cpl_size tny = cpl_image_get_size_y(aTargetImage->data);
  float *tdata = cpl_image_get_data_float(aTargetImage->data);
  float *tstat = cpl_image_get_data_float(aTargetImage->stat);

  cpl_size snx = cpl_image_get_size_x(aSourceImage->data);
  cpl_size sny = cpl_image_get_size_y(aSourceImage->data);
  float *sdata = cpl_image_get_data_float(aSourceImage->data);
  const cpl_mask *smask = cpl_image_get_bpm_const(aSourceImage->data);
  const cpl_binary *sbpm = cpl_mask_get_data_const(smask);

  float *sstat = NULL;
  if (aSourceImage->stat) {
    sstat = cpl_image_get_data_float(aSourceImage->stat);
  }

  /* Get the WCS information for the source image from the header. This  *
   * WCS has the sky coordinates of the origin given in degrees and is    *
   * needed for the conversion from pixel coordinates to sky coordinates. */
  muse_wcs *swcs = muse_wcs_new(aSourceImage->header);
  swcs->iscelsph = CPL_TRUE;

  /* Create a clone of the source WCS where the sky coordinates are given  *
   * in radians. This is needed for the projection back to the pixel plane *
   * and is done once here to avoid the repeated computations in the loop. */
  muse_wcs swcsrad;
  memcpy(&swcsrad, swcs, sizeof *swcs);
  swcsrad.crval1 *= CPL_MATH_RAD_DEG;
  swcsrad.crval2 *= CPL_MATH_RAD_DEG;

  double xnorm = 1.;
  double ynorm = 1.;
  muse_wcs_get_scales(aSourceImage->header, &xnorm, &ynorm);
  xnorm = 1. / xnorm;
  ynorm = 1. / ynorm;

  cpl_image *tmask = cpl_image_new(tnx, tny, CPL_TYPE_INT);
  cpl_image_fill_window(tmask, 1, 1, tnx, tny, EURO3D_MISSDATA);
  int *tdq = cpl_image_get_data_int(tmask);

  cpl_size iy;
  for (iy = 0; iy < tny; ++iy) {
    cpl_size ix;
    for (ix = 0; ix < tnx; ++ix) {
      double ra;
      double dec;
      muse_wcs_celestial_from_pixel_fast(aTargetWCS, ix + 1, iy + 1, &ra, &dec);

      double x;
      double y;
      muse_wcs_pixel_from_celestial_fast(&swcsrad, ra * CPL_MATH_RAD_DEG,
                                         dec * CPL_MATH_RAD_DEG , &x, &y);

      /* Find nearest neighbor */
      cpl_size kx = (cpl_size)floor(x);
      cpl_size ky = (cpl_size)floor(y);

      cpl_size lx = kx;
      cpl_size ly = ky;
      double dmin = DBL_MAX;
      double xscale = cos(dec * CPL_MATH_RAD_DEG);

      cpl_size jy;
      for (jy = -1; jy <= 1; ++jy) {
        cpl_size jx;
        for (jx = -1; jx <= 1; ++jx) {
          double _ra;
          double _dec;
          muse_wcs_celestial_from_pixel_fast(swcs, kx + jx, ky + jy,
                                             &_ra, &_dec);
          double dx = xnorm * fabs(ra - _ra) * xscale;
          double dy = ynorm * fabs(dec - _dec);
          double distance = sqrt(dx * dx + dy * dy);
          if (distance < dmin) {
            dmin = distance;
            lx = kx + jx;
            ly = ky + jy;
          }
        }
      }
      if (((lx > 0) && (lx <= snx)) && ((ly > 0) && (ly <= sny))) {
        cpl_size toffset = tnx * iy + ix;
        cpl_size soffset = snx * (ly - 1) + (lx - 1);
        if (!sbpm || (sbpm && !sbpm[soffset])) {
          tdata[toffset] += sdata[soffset];
          tdq[toffset] = EURO3D_GOODPIXEL;
          if (sstat) {
            tstat[toffset] += sstat[soffset];
          }
        }
      }
    }
  }

  /* Update data quality mask of the target image */
  if (aTargetImage->dq) {
    int *dq = cpl_image_get_data_int(aTargetImage->dq);
    cpl_size npixel = tnx * tny;
    cpl_size ipixel;
    for (ipixel = 0; ipixel < npixel; ++ipixel) {
      dq[ipixel] = (dq[ipixel] && tdq[ipixel]) ?
          EURO3D_MISSDATA : EURO3D_GOODPIXEL;
    }
  } else {
    aTargetImage->dq = tmask;
    tmask = NULL;
  }
  cpl_image_delete(tmask);
  cpl_free(swcs);

  return CPL_ERROR_NONE;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Create a mosaic from a list of FOV images.
  @param    aImagelist   the list of FOV images
  @param    aOffsets     the table of coordinate offsets of the individual images.
  @return   an new image containing the union of the field-of-view of all
            input FOV images, or NULL if an error occurred.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aImagelist or aOffsets is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         aImagelist is empty, aOffset does not provide the columns
         RA_OFFSET or DEC_OFFSET}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         The size of aImagelist and the number of rows of aOffsets do
         not match}
  @error{set CPL_ERROR_UNSUPPORTED_MODE\, return NULL,
         The list of input images is empty, or the number of input images
         does not match the number of offsets after the field-of-view limits
         were applied}

  Create a field-of-view mosaic image from a list of individual field-of-view
  images and their relative coordinate offsets with respect to the first image
  in the input image list. Note that therefore the function expects that the
  offsets of the first image in aImagelist, i.e. the offset entries in the
  first row of aOffsets are zero!

  The function first corrects the coordinate system of the individual
  field-of-view images by applying the corresponding coordinate offsets to
  the origin of the WCS stored in the image header. Input images having offsets
  larger than 0.5 degrees in right ascension or declination, with respect to
  the first exposure, are discarded and do not contribute to the combined
  field-of-view image.

  From the coordinate systems taken from the header of the input images
  the total extent of the combined field-of-view is determined comparing the
  sky coordinates of the four corners of the input images.

  Using the WCS of the first input image as a reference the WCS of the
  mosaic is constructed, with the origin placed in the center of the combined
  field-of-view.

  Finally the input images are projected onto the target WCS grid using
  a nearest neighbor interpolation and co-added. Pixels in the mosaic image
  which have never been set during the resampling step to a valid pixel value
  are flagged in the bad pixel masks of the data image and the variance image
  of the resulting, combined field-of-view image.
 */
/*----------------------------------------------------------------------------*/
static muse_image *
muse_align_create_mosaic(muse_imagelist *aImagelist, cpl_table *aOffsets)
{
  cpl_ensure(aImagelist && aOffsets, CPL_ERROR_NULL_INPUT, NULL);

  cpl_size _nexposures = muse_imagelist_get_size(aImagelist);
  cpl_ensure((cpl_table_has_column(aOffsets, MUSE_OFFSETS_DRA) &&
             cpl_table_has_column(aOffsets, MUSE_OFFSETS_DDEC)),
             CPL_ERROR_ILLEGAL_INPUT, NULL);
  cpl_ensure(_nexposures > 0, CPL_ERROR_ILLEGAL_INPUT, NULL);
  cpl_ensure(_nexposures == cpl_table_get_nrow(aOffsets),
             CPL_ERROR_INCOMPATIBLE_INPUT, NULL);
  cpl_ensure(((cpl_table_get_double(aOffsets, MUSE_OFFSETS_DRA, 0, NULL) < DBL_EPSILON) &&
             (cpl_table_get_double(aOffsets, MUSE_OFFSETS_DDEC, 0, NULL) < DBL_EPSILON)),
             CPL_ERROR_ILLEGAL_INPUT, NULL);

  /* Maximum size in degrees of the field-of-view along the right ascension  *
   * and the declination axis. For NFM mode observations this is scaled      *
   * down by the ratio of the MUSE spaxel sizes. The mode is taken from the *
   * first exposure in the input list.                                      */

  /* XXX: Make fovSizeMax adjustable via a parameter? */
  double fovSizeMaxX = 1.;
  double fovSizeMaxY = fovSizeMaxX;
  muse_ins_mode mode = muse_pfits_get_mode(muse_imagelist_get(aImagelist, 0)->header);
  if (mode == MUSE_MODE_NFM_AO_N) {
    fovSizeMaxX *= kMuseSpaxelSizeX_NFM / kMuseSpaxelSizeX_WFM;
    fovSizeMaxY *= kMuseSpaxelSizeY_NFM / kMuseSpaxelSizeY_WFM;
  }

  const char *modename = (mode == MUSE_MODE_NFM_AO_N) ? "NFM" : "WFM";
  cpl_msg_info(__func__, "Selecting exposures within the %s mode limits of "
               "%.3f times %.3f degrees for the combined field-of-view, "
               "using the first exposure as reference!",
               modename, fovSizeMaxX, fovSizeMaxY);

  /* Create a filtered copy of the input list of images. Exposures   *
   * where the offset in either of the two axis exceeds the given    *
   * maximum field-of-view half width are ignored. This effectively  *
   * removes exposures from the input list which have excessively    *
   * large offsets because of an unreliable offset computation, which *
   * would cause memory allocation errors later in the processing.    */
  muse_imagelist *images = muse_imagelist_new();
  cpl_table *offsets = cpl_table_duplicate(aOffsets);
  cpl_table_unselect_all(offsets);
  cpl_size kexposure = 0;
  cpl_size iexposure;
  for (iexposure = 0; iexposure < _nexposures; ++iexposure) {
    double shift_ra  = cpl_table_get_double(offsets, MUSE_OFFSETS_DRA,
                                            iexposure, NULL);
    double shift_dec = cpl_table_get_double(offsets, MUSE_OFFSETS_DDEC,
                                            iexposure, NULL);
    cpl_boolean inDomainRA  = fabs(shift_ra) < 0.5 * fovSizeMaxX;
    cpl_boolean inDomainDEC = fabs(shift_dec) < 0.5 * fovSizeMaxY;

    if ((isfinite(shift_ra) && inDomainRA) && (isfinite(shift_dec) && inDomainDEC)) {
      muse_image *_image = muse_imagelist_get(aImagelist, iexposure);
      muse_imagelist_set(images, muse_fov_duplicate(_image), kexposure);
      ++kexposure;
    } else {
      cpl_table_select_row(offsets, iexposure);

      cpl_msg_warning(__func__, "Offsets of exposure %" CPL_SIZE_FORMAT
                      " (dRA = %.6e [deg], dDEC = %.6e [deg]) are beyond "
                      "the limits. Ignoring exposure!", iexposure + 1,
                      shift_ra, shift_dec);
    }
  }

  /* Remove exposures with excessive field offsets from the offset list *
   * to match the contents of the filtered image list. Reset the number *
   * of exposures to the size of the reduced list.                      */
  cpl_table_erase_selected(offsets);
  cpl_size nfields = cpl_table_get_nrow(offsets);
  cpl_size nexposures = muse_imagelist_get_size(images);
  if (nexposures != nfields) {
    cpl_msg_error(__func__, "List of input images does not match the list "
                  "of field offsets after removing exposures with excessive "
                  "offsets!");
    cpl_error_set_message(__func__, CPL_ERROR_UNSUPPORTED_MODE,
                          "Number of input exposures does not match the "
                          "number of field offset entries after applying "
                          "the field-of-view limits!");
    cpl_table_delete(offsets);
    muse_imagelist_delete(images);
    return NULL;
  }
  if (nfields < 1) {
    cpl_msg_error(__func__, "No exposures are left after removing fields "
                  "with excessive offsets!");
    cpl_error_set_message(__func__, CPL_ERROR_UNSUPPORTED_MODE,
                          "Empty list of input exposures after applying "
                          "the field-of-view limits!");
    cpl_table_delete(offsets);
    muse_imagelist_delete(images);
    return NULL;
  }
  if (nfields == 1) {
    cpl_msg_warning(__func__, "Only the zero offset exposure (reference "
                    "image) is left after removing fields with excessive "
                    "offsets!");

  }
  cpl_msg_info(__func__, "Creating mosaic from %" CPL_SIZE_FORMAT
               " of %" CPL_SIZE_FORMAT " exposures", nexposures, _nexposures);

  /* Correct the offsets of the individual images in the input list */
  cpl_msg_info(__func__, "Applying WCS offsets to individual exposures");
  if (muse_align_correct_offsets(images, offsets) != CPL_ERROR_NONE) {
    cpl_table_delete(offsets);
    muse_imagelist_delete(images);
    cpl_msg_error(__func__, "Correcting image offsets failed: %s",
                  cpl_error_get_message());
    return NULL;
  }
  cpl_table_delete(offsets);

  /* Compute the field-of-view region covered by the mosaic */
  cpl_msg_info(__func__, "Computing the size of the combined FOV");
  double ra[2];
  double dec[2];
  cpl_error_code status = muse_align_mosaic_get_fov(images, &ra[0], &dec[0],
                                                   &ra[1], &dec[1]);
  if (status != CPL_ERROR_NONE) {
    cpl_msg_error(__func__, "Field-of-view size computation failed: %s",
                  cpl_error_get_message());
    muse_imagelist_delete(images);
    return NULL;
  }
  cpl_msg_info(__func__, "Computed field-of-view limits: RA = [%.6f, %.6f] "
               "DEC = [%.6f, %.6f]", ra[0], ra[1], dec[0], dec[1]);

  /* Use the WCS of the first exposure as the target WCS *
   * of the field-of-view mosaic.                        */
  cpl_msg_info(__func__, "Creating target WCS from the first exposure");
  muse_image *image = muse_imagelist_get(images, 0);
  if (!muse_align_wcs_is_gnomonic(image->header)) {
    cpl_msg_error(__func__, "Found unsupported WCS in the first exposure."
                  " Not a gnomonic WCS!");
    muse_imagelist_delete(images);
    return NULL;
  }

  double nx;
  double ny;
  muse_wcs *wcs = muse_align_mosaic_setup_wcs(image->header, ra[0], dec[0],
                                              ra[1], dec[1], &nx, &ny);
  if (!wcs) {
    cpl_msg_error(__func__, "Creating mosaic WCS failed: %s",
                  cpl_error_get_message());
    muse_imagelist_delete(images);
    return NULL;
  }
  cpl_msg_debug(__func__, "Target WCS origin at pixel position (X, Y) = "
                "(%.6f, %.6f) with (RA, DEC) = (%.6f, %.6f)", wcs->crpix1,
                wcs->crpix2, wcs->crval1, wcs->crval2);
  double t_ra[4];
  double t_dec[4];
  muse_wcs_celestial_from_pixel_fast(wcs, 1, 1,   &t_ra[0], &t_dec[0]);
  muse_wcs_celestial_from_pixel_fast(wcs, 1, ny,  &t_ra[1], &t_dec[1]);
  muse_wcs_celestial_from_pixel_fast(wcs, nx, 1,  &t_ra[2], &t_dec[2]);
  muse_wcs_celestial_from_pixel_fast(wcs, nx, ny, &t_ra[3], &t_dec[3]);
  cpl_msg_debug(__func__, "FOV limits from final WCS: "
                "lower-left (RA, DEC) = (%.6f, %.6f), "
                "upper-left (RA, DEC) = (%.6f, %.6f), "
                "lower-right (RA, DEC) = (%.6f, %.6f), "
                "upper-right (RA, DEC) = (%.6f, %.6f), ",
                t_ra[0], t_dec[0],
                t_ra[1], t_dec[1],
                t_ra[2], t_dec[2],
                t_ra[3], t_dec[3]);

  /* Project the input images onto the WCS of the target image */
  muse_image *mosaic = muse_image_new();
  mosaic->header = cpl_propertylist_duplicate(image->header);
  cpl_propertylist_set_double(mosaic->header, "CRPIX1", wcs->crpix1);
  cpl_propertylist_set_double(mosaic->header, "CRPIX2", wcs->crpix2);
  cpl_propertylist_set_double(mosaic->header, "CRVAL1", wcs->crval1);
  cpl_propertylist_set_double(mosaic->header, "CRVAL2", wcs->crval2);

  mosaic->data = cpl_image_new(nx, ny, CPL_TYPE_FLOAT);
  mosaic->stat = cpl_image_new(nx, ny, CPL_TYPE_FLOAT);
  mosaic->dq = NULL;

  cpl_errorstate estate = cpl_errorstate_get();
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    muse_image *_image = muse_imagelist_get(images, iexposure);

    /* Merge any leftover data quality extension into the bad pixel *
     * mask of the data image.                                      */
    if (_image->dq) {
      muse_quality_image_reject_using_dq(_image->data, _image->dq, NULL);
      cpl_image_delete(_image->dq);
      _image->dq = NULL;
    }
    muse_align_resampling_image_nearest_add(wcs, mosaic, _image);
  }
  if (!cpl_errorstate_is_equal(estate)) {
    cpl_msg_error(__func__, "Resampling %" CPL_SIZE_FORMAT " images onto "
                  "the target WCS grid failed!", nexposures);
    muse_image_delete(mosaic);
    muse_imagelist_delete(images);
    return NULL;
  }
  cpl_free(wcs);
  muse_imagelist_delete(images);

  /* Update the bad pixel masks with the contents of the data *
   * quality mask                                            */
  muse_quality_image_reject_using_dq(mosaic->data, mosaic->dq, mosaic->stat);

  return mosaic;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Compute coordinate offset for each exposure with respect to the
            reference.
  @param    aProcessing  the processing structure
  @param    aParams      the parameters list
  @return   0 if everything is ok, -1 something went wrong

 */
/*----------------------------------------------------------------------------*/
int
muse_exp_align_compute(muse_processing *aProcessing,
                       muse_exp_align_params_t *aParams)
{

  cpl_size nexposures = cpl_frameset_count_tags(aProcessing->inframes,
                                                MUSE_TAG_IMAGE_FOV);
  if (nexposures < 2) {
    cpl_msg_error(__func__, "This recipe requires at least 2 input FOV "
                  "images!");
    return -1;
  }

  /* Check whether the source detection task should be run or if the *
   * source positions are provided as part of the input data set.     */
  cpl_size npositions = cpl_frameset_count_tags(aProcessing->inframes,
                                                MUSE_TAG_SOURCE_LIST);
  cpl_boolean override_detection = aParams->override_detection && (npositions > 0);


  /* Load and validate all fov input images */
  cpl_msg_info(__func__, "Loading FOV images");

  muse_imagelist *fovimages = muse_processing_fov_load_all(aProcessing);
  if (!fovimages) {
    cpl_msg_error(__func__, "Could not load FOV input images!");
    return -1;
  }
  nexposures = (cpl_size)muse_imagelist_get_size(fovimages);

  cpl_size iexposure;
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    const muse_image *fov = muse_imagelist_get(fovimages, iexposure);
    cpl_errorstate es = cpl_errorstate_get();
    double mjdobs = muse_pfits_get_mjdobs(fov->header);

    if (!cpl_errorstate_is_equal(es) &&
        cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND) {
      cpl_msg_error(__func__, "Missing \"MJD-OBS\" in FOV image %"
                    CPL_SIZE_FORMAT, iexposure + 1);
      muse_imagelist_delete(fovimages);
      return -1;
    } else {
      cpl_size jexposure;
      for (jexposure = iexposure + 1; jexposure < nexposures; ++jexposure) {
        const muse_image *_fov = muse_imagelist_get(fovimages, jexposure);
        double _mjdobs = muse_pfits_get_mjdobs(_fov->header);
        if (_mjdobs == mjdobs) {
          cpl_msg_warning(__func__, "Found FOV images with non-unique "
                          "\"MJD-OBS\" value (image %" CPL_SIZE_FORMAT
                          " and %" CPL_SIZE_FORMAT ")!", iexposure + 1,
                          jexposure + 1);
        }
      }
    }
  } /* for iexposure */

  /* Cleanup NaN values which are possibly present in the input images to *
   * flag bad pixel. Replace them with an appropriate numerical value     */
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    muse_image *image = muse_imagelist_get(fovimages, iexposure);
    cpl_image_reject_value(image->data, CPL_VALUE_NOTFINITE);
    muse_utils_replace_nan(image, 0.);
  }

  /* Prepare the source catalogs for each of the input FOV images. Either *
   * the sources are detected running a source detection task on each of  *
   * the input FOV images, or the source positions are read from catalogs *
   * being provided through the input data set.                           */

  /* Array of source lists. It must have the same order as the list of *
   * input FOV images when it is used to compute the field offsets!    */
  muse_table **srclists = NULL;

  if (override_detection) {
    cpl_msg_info(__func__, "Found %" CPL_SIZE_FORMAT
                 " source lists in the input data set!", npositions);

    /* For each input FOV image there must be a corresponding catalog *
     * of source positions.                                          */
    if (npositions != nexposures) {
      muse_imagelist_delete(fovimages);
      cpl_msg_error(__func__, "Number of input source lists does not match "
                    "the number of FOV images!");
      return -1;
    }

    srclists = muse_align_load_catalogs(aProcessing, fovimages);
    if (!srclists) {
      muse_imagelist_delete(fovimages);
      cpl_msg_error(__func__, "Preparing the input source lists for %"
                    CPL_SIZE_FORMAT " FOV images failed!", nexposures);
      return -1;
    }
  } else {
    cpl_msg_info(__func__, "Detecting point-sources on %" CPL_SIZE_FORMAT
                 " FOV images", nexposures);

    /* Validate input parameters */
    cpl_msg_debug(__func__, "Validating source detection parameters");
    if (!muse_align_check_detection_params(aParams)) {
      cpl_error_set_where(__func__);
      muse_imagelist_delete(fovimages);
      return -1;
    }

    srclists = muse_align_detect_sources(fovimages, aParams);
    if (!srclists) {
      muse_imagelist_delete(fovimages);
      cpl_msg_error(__func__, "Detection of point-sources failed!");
      return -1;
    }

    /* Remove sources from the source lists if they are unreliable, i.e. if   *
     * they are to close to the FOV edges. The FOV images are padded by NaN    *
     * values which are propagated to the bad pixel mask when the FOV images   *
     * are loaded. The criterion for being to close to the FOV edge the       *
     * distance to the next bad pixel is used. Note that this may also discard *
     * in the central part of the field of view. This should have no impact   *
     * long as there are enough sources left.                                 */

    const double distance = ceil(aParams->bpixdistance);

    for (iexposure = 0; iexposure < nexposures; ++iexposure) {
      muse_image *image = muse_imagelist_get(fovimages, iexposure);
      muse_table *srclist = srclists[iexposure];
      cpl_size nsources = cpl_table_get_nrow(srclist->table);

      cpl_msg_info(__func__, "Filtering source list of image %"
                   CPL_SIZE_FORMAT ": discarding sources closer to bad pixels "
                   "than %.0f pixel", iexposure + 1, distance);
      cpl_size ndiscarded = muse_align_clean_source_list(srclist, image,
                                                        distance);
      cpl_msg_info(__func__, "Discarding %" CPL_SIZE_FORMAT " sources "
                   "detected in image %" CPL_SIZE_FORMAT, ndiscarded,
                    iexposure + 1);
      if (nsources - ndiscarded < 1) {
        muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
        muse_imagelist_delete(fovimages);
        cpl_msg_error(__func__, "Too few sources left after filtering detected "
                      "sources of image %" CPL_SIZE_FORMAT, iexposure + 1);
        return -1;
      }
    }
  } /* if override_detection */

  /* Compute exposure offsets */
  cpl_msg_info(__func__, "Computing pointing corrections for %" CPL_SIZE_FORMAT
               " FOV images", nexposures);

  cpl_array *radii = muse_align_parse_valuelist(aParams->rsearch);
  if (!radii) {
    muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
    muse_imagelist_delete(fovimages);
    cpl_msg_error(__func__, "Cannot compute field offsets! No valid search "
                  "radius was given!");
    return -1;
  }

  /* XXX: Dealing with subsets of the input fields should be better *
   *      integrated into the code structure, possibly done by      *
   *      muse_align_compute_field_offsets().                       */

  /* Before the field offsets are calculated the fields and the source *
   * list with no detected sources have to be filtered out. They will  *
   * be added again later when the final offset table is prepared.     *
   * Their corresponding field offsets will be forced to zero.         */

  muse_imagelist *_fovimages = muse_imagelist_new();
  muse_table **_srclists = cpl_calloc(nexposures, sizeof *_srclists);
  cpl_array *field_id = cpl_array_new(nexposures, CPL_TYPE_SIZE);
  cpl_propertylist *header = cpl_propertylist_new();

  cpl_size kexposures = 0;
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    if (cpl_table_get_nrow(srclists[iexposure]->table) > 0) {
      muse_imagelist_set(_fovimages, muse_imagelist_get(fovimages, iexposure),
                         kexposures);
      _srclists[kexposures] = srclists[iexposure];
      cpl_array_set(field_id, kexposures, iexposure);
      ++kexposures;
    } else {
      char *kw = cpl_sprintf(QC_ALIGN_NDETi, (int)iexposure + 1);
      cpl_propertylist_append_int(header, kw, 0);
      cpl_free(kw);

      kw = cpl_sprintf(QC_ALIGN_NMATCHi, (int)iexposure + 1);
      cpl_propertylist_append_int(header, kw, 0);
      cpl_free(kw);

      cpl_msg_warning(__func__, "No sources are available for image % "
                      CPL_SIZE_FORMAT ". This field will be ignored and no "
                      "coordinate offsets will be calculated!", iexposure + 1);
    }
  }
  _srclists = cpl_realloc(_srclists, kexposures * sizeof *_srclists);
  muse_cplarray_erase_invalid(field_id);

  if (kexposures < 2) {
    cpl_propertylist_delete(header);
    cpl_array_delete(field_id);
    muse_vfree((void **)_srclists, kexposures, (muse_free_func)muse_table_delete);
    muse_imagelist_delete(_fovimages);
    muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
    muse_imagelist_delete(fovimages);
    cpl_msg_error(__func__, "Cannot compute field offsets! Source positions "
                  "are only available for %" CPL_SIZE_FORMAT " out of %"
                  CPL_SIZE_FORMAT " fields!", kexposures, nexposures);
    return -1;
  }

  cpl_msg_info(__func__, "Calculating field offsets...");
  cpl_table *offsets = muse_align_compute_field_offsets(_fovimages, _srclists,
                                                        field_id, radii,
                                                        aParams->nbins,
                                                        aParams->weight,
                                                        header);

  /* XXX: Update the number of fields without matches only after *
   *      calling muse_align_compute_field_offsets(). Otherwise  *
   *      this value would be overwritten.                       */

  int nomatch = cpl_propertylist_get_int(header, QC_ALIGN_NOMATCH) +
      nexposures - kexposures;
  cpl_propertylist_update_int(header, QC_ALIGN_NOMATCH, nomatch);


  /* When cleaning up the filtered image list and the list of source lists *
   * just clean up the container, as the actual data is managed by their   *
   * full versions. The filtered data structures just served as a view for *
   * the computation of the offsets.                                       */

  cpl_free(_srclists);
  iexposure = muse_imagelist_get_size(_fovimages);
  while (iexposure) {
    (void)muse_imagelist_unset(_fovimages, 0);
    --iexposure;
  }
  muse_imagelist_delete(_fovimages);

  cpl_array_delete(radii);
  if (!offsets) {
    cpl_propertylist_delete(header);
    cpl_array_delete(field_id);
    muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
    muse_imagelist_delete(fovimages);
    cpl_msg_error(__func__, "Could not compute FOV offsets for %"
                  CPL_SIZE_FORMAT " images", nexposures);
    return -1;
  }

  /* Finalize the source list product, and add columns with the corrected *
   * source positions to the source lists.                               */
  for (iexposure = 0; iexposure < cpl_array_get_size(field_id); ++iexposure) {
    int invalid[2] = {0, 0};
    double shift_ra = cpl_table_get_double(offsets, MUSE_OFFSETS_DRA,
                                           iexposure, &invalid[0]);
    double shift_dec = cpl_table_get_double(offsets, MUSE_OFFSETS_DDEC,
                                            iexposure, &invalid[1]);

    cpl_size ifield = cpl_array_get(field_id, iexposure, NULL);

    if (invalid[0] || invalid[1]) {
      cpl_msg_warning(__func__, "Invalid field offsets found for field %"
                    CPL_SIZE_FORMAT ". No Correction will be applied to "
                    "the position of the detected sources.", ifield);
    }
    else {
      cpl_table_subtract_scalar(srclists[ifield]->table, MUSE_SRCLIST_RACORR,
                                shift_ra);
      cpl_table_subtract_scalar(srclists[ifield]->table, MUSE_SRCLIST_DECCORR,
                                shift_dec);
    }
  }

  cpl_array_delete(field_id);

  /* Create the optional preview exposure map by creating individual *
   * exposure maps from the input FOV images and projecting the      *
   * individual exposure maps onto a common WCS grid.                */
  muse_image *expmap = NULL;
  if (aParams->expmap) {
    muse_imagelist *expmaps = muse_align_create_expmaps(fovimages);
    if (!expmaps) {
      cpl_msg_error(__func__, "Creating exposure maps for %" CPL_SIZE_FORMAT
                    " exposures failed!", nexposures);
      cpl_propertylist_delete(header);
      cpl_table_delete(offsets);
      muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
      muse_imagelist_delete(fovimages);
      return -1;
    }

    /* Combine the exposure maps */
    expmap = muse_align_create_mosaic(expmaps, offsets);
    if (!expmap) {
      cpl_msg_error(__func__, "Combining exposure maps for %" CPL_SIZE_FORMAT
                    " exposures failed!", nexposures);
      cpl_propertylist_delete(header);
      cpl_table_delete(offsets);
      muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
      muse_imagelist_delete(fovimages);
      return -1;
    }

    /* If present remove the variance image from the exposure map as it has *
     * no real meaning in this case.                                        */
    if (expmap->stat) {
      cpl_image_delete(expmap->stat);
      expmap->stat = NULL;
    }

    cpl_propertylist_update_float(expmap->header, "ESO QC EXPALIGN EXPTIME MIN",
                                  cpl_image_get_min(expmap->data));
    cpl_propertylist_update_float(expmap->header, "ESO QC EXPALIGN EXPTIME MAX",
                                  cpl_image_get_max(expmap->data));
    cpl_propertylist_update_float(expmap->header, "ESO QC EXPALIGN EXPTIME AVG",
                                  cpl_image_get_mean(expmap->data));
    cpl_propertylist_update_float(expmap->header, "ESO QC EXPALIGN EXPTIME MED",
                                  cpl_image_get_median(expmap->data));

    /* Encode the data quality mask of the exposure map as NaN values in   *
     * the image data so that it uses the same file structure as the input *
     * FOV images.                                                        */
    muse_image_dq_to_nan(expmap);
    muse_imagelist_delete(expmaps);
  }

  /* Write offset table product to file */
  cpl_error_code rc = muse_processing_save_table(aProcessing, -1, offsets,
                                                 header, MUSE_TAG_OFFSET_LIST,
                                                 MUSE_TABLE_TYPE_CPL);
  cpl_propertylist_delete(header);
  cpl_table_delete(offsets);

  cpl_errorstate status = cpl_errorstate_get();

  /* Save source list products to file. Note that in order to get the *
   * header information from FOV image associated to the source list  *
   * product it has to be saved using CPL calls directly.             */
  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    muse_processing_save_table(aProcessing, -1, srclists[iexposure]->table,
                               srclists[iexposure]->header,
                               MUSE_TAG_SOURCE_LIST, MUSE_TABLE_TYPE_CPL);

  }
  muse_vfree((void **)srclists, nexposures, (muse_free_func)muse_table_delete);
  muse_imagelist_delete(fovimages);

  /* Save the optionally created exposure map product to file */
  if (expmap) {
    muse_processing_save_image(aProcessing, -1, expmap, "EXPOSURE_MAP");
    muse_image_delete(expmap);
  }

  if ((rc != CPL_ERROR_NONE) || !cpl_errorstate_is_equal(status)) {
    return -1;
  }

  return 0;
} /* muse_exp_align_compute() */
