/* -*- 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) 2001-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 <inttypes.h>
#include <complex.h>
#include <math.h>

#include "muse_cplwrappers.h"
#include "muse_utils.h"
#include "muse_findstars.h"

/*----------------------------------------------------------------------------*
 *                             Debugging/feature Macros                       *
 *----------------------------------------------------------------------------*/
#define DEBUG_MUSE_FINDSTARS 0  /* if 1 enable debugging of muse_find_stars() */

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_findstar     2D Source Detection
 *
 * This group provides a simple 2d image source detection based of the
 * DAOFIND algorithm (Stetson P.B., 1987, PASP 99, 191).
 *
 * This implementation of the DAOFIND algorithm is largely based on the
 * C implementation of the IDL find routine which was developed by
 * A. Pecontal to be used in the MUSE Instrument Control Software.
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*
 *            Candidate CPL extensions (use CPL coding style!)                *
 *----------------------------------------------------------------------------*/

/*
 * @brief
 *  Compute the sum of the matrix elements in each column.
 *
 * @param self  A matrix object.
 *
 * @return
 *   On success the function returns a matrix of the sums of the elements of
 *   each matrix column. In case an error occurs @c NULL is returned.
 *
 * The function computes the sum of the matrix elements of each column vector
 * of the matrix @em self. It returns a row vector, i.e. a matrix object made
 * up of a single row, whose elements are the computed sums of the matrix
 * elements.
 */

static cpl_matrix *
_cpl_matrix_sum_columns(const cpl_matrix *self)
{

    cpl_matrix *vector = NULL;

    if (self) {

        cpl_size nr = cpl_matrix_get_nrow(self);
        cpl_size nc = cpl_matrix_get_ncol(self);

        vector = cpl_matrix_extract(self, 0, 0, 1, 1, 1, nc);

        const double *_self = cpl_matrix_get_data_const(self);

        double *_vector = cpl_matrix_get_data(vector);

        register cpl_size ir;


        for (ir = 1; ir < nr; ++ir) {

            register cpl_size ic;

            for (ic = 0; ic < nc; ++ic) {
                _vector[ic] += _self[ir * nc + ic];
            }

        }

    }

    return vector;

}


/*
 * @brief
 *  Compute the sum of the matrix elements in each row.
 *
 * @param self  A matrix object.
 *
 * @return
 *   On success the function returns a matrix of the sum of the elements of
 *   each matrix row. In case an error occurs @c NULL is returned.
 *
 * The function computes the sum of the matrix elements of each row vector
 * of the matrix @em self. It returns a column vector, i.e. a matrix object
 * made up of a single column, whose elements are the computed sums of the
 * matrix elements.
 */

static cpl_matrix *
_cpl_matrix_sum_rows(const cpl_matrix *self)
{

    cpl_matrix *vector = NULL;

    if (self) {

        cpl_size nr = cpl_matrix_get_nrow(self);
        cpl_size nc = cpl_matrix_get_ncol(self);

        vector = cpl_matrix_extract(self, 0, 0, 1, 1, nr, 1);

        const double *_self   = cpl_matrix_get_data_const(self);

        double *_vector = cpl_matrix_get_data(vector);

        register cpl_size ir;


        for (ir = 0; ir < nr; ++ir) {

            register cpl_size ic;

            for (ic = 1; ic < nc; ++ic) {
                _vector[ir] += _self[ir * nc + ic];
            }

        }

    }

    return vector;

}


/*
 * @brief
 *  Compute the total sum of the elements of a matrix..
 *
 * @param self  A matrix object.
 *
 * @return
 *   On success the function returns the sum of all matrix elements. If an
 *   error occurs @c 0. is returned and an appropriate error code is set.
 *
 * The function computes the sum of all matrix elements.
 */

static double
_cpl_matrix_sum(const cpl_matrix *self)
{

    double sum = 0.;

    if (self) {

        register cpl_size i;

        cpl_size sz = cpl_matrix_get_nrow(self) * cpl_matrix_get_ncol(self);

        const double *_self = cpl_matrix_get_data_const(self);


        for (i = 0; i < sz; ++i) {
            sum += _self[i];
        }

    }

    return sum;

}


/*
 * @brief
 *  Compute the remainder of the element-wise, Euclidean division by a scalar.
 *
 * @param self   A matrix object.
 * @param value  The integral dividend.
 *
 * @return
 *   On success the function returns @c CPL_ERROR_NONE, and an appropriate
 *   error code otherwise.
 *
 * The function computes, in place, the remainder of the Euclidean division of
 * each matrix element by the dividend @em value. The Euclidean division is
 * done by casting each matrix element to an integer values without rounding
 * before dividing it by @em value.
 */

static cpl_error_code
_cpl_matrix_modulo(cpl_matrix *self, long value)
{

    if (value == 0.) {
        return CPL_ERROR_DIVISION_BY_ZERO;
    }

    cpl_size sz = cpl_matrix_get_nrow(self) * cpl_matrix_get_ncol(self);

    double *m = cpl_matrix_get_data(self);


    while (sz--) {
        *m = (long)(*m) % value;
        ++m;
    }

    return CPL_ERROR_NONE;

}


/*----------------------------------------------------------------------------*
 *                           Private Functions                                *
 *----------------------------------------------------------------------------*/

typedef int (*_cpl_matrix_element_compare_func)(double aValue1, double aValue2);


static int
_muse_condition_not_equal(double aValue1, double aValue2)
{
  return (aValue1 != aValue2) ? TRUE : FALSE;
}

static int
_muse_condition_greater_equal(double aValue1, double aValue2)
{
  return (aValue1 >= aValue2) ? TRUE : FALSE;
}


/* XXX: Do style cleanup and improve documentation */
/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Compute the relative offset of the centroid position.
  @param  offset  Result centroid position offset along the selected axis.
  @param  axis    Axis selector (1: x-axis, 2: y-axis)
  @param  mdata   Matrix view of search box sized subimage located at estimated
                  source position.
  @param  mwt     Vector of Pixel weights (1 x nbox matrix, where nbox
                  is the search box size).
  @param  mwtxy   Matrix of pixel weights along the non-selected axis
                  (nbox x nbox matrix).
  @param  msgxy   Matrix of sums of the weighted convolution kernel (sums
                  carried out along the non-selected axis).
  @param  mdg     Derivative of the marginal Gaussian along the selected axis.
  @param  sumg    Sum of the elements of the element-by-element product
                  mwt * msgxy.
  @param  sumgsqr Element-by-element product mwt * msgxy**2
  @param  sgdg    Element-by-element product msgxy * mdg.
  @param  sdg     Sum of the elements of the element-by-element product
                  mwt * mdg
  @param  sdgs    Sum of the elements of the element-by-element product
                  mwt * mdg**2
  @param  p       Total sum of weights.
  @param  sigsqr  sigma squared of the Gaussian convolution kernel.
  @return The return value is 0 on success, and a non-zero value if an error
          occurred.

  Utility function encapsulating the centroid offset computation along a given
  axis.
 */
/*----------------------------------------------------------------------------*/
static int
_muse_centroid_offset(double *offset, unsigned short axis,
                      const cpl_matrix *mdata, const cpl_matrix *mwt,
                      const cpl_matrix *mwtxy, const cpl_matrix *msgxy,
                      const cpl_matrix *mdg, double sumg, double sumgsqr,
                      double sgdg, double sdg, double sdgs,
                      double p, double sigsqr)
{
  double sddg    = 0.;
  double sumd    = 0.;
  double sumgd   = 0.;

  if ((axis != 1) && (axis != 2)) {
    return -1;
  }

  cpl_matrix *mtmp = muse_cplmatrix_multiply_create(mdata, mwtxy);

  cpl_matrix *sd;
  if (axis == 1) {
    sd = _cpl_matrix_sum_columns(mtmp);
  }
  else {
    cpl_matrix *_sd = _cpl_matrix_sum_rows(mtmp);
    sd = cpl_matrix_transpose_create(_sd);
    cpl_matrix_delete(_sd);
  }
  cpl_matrix_delete(mtmp);


  mtmp = muse_cplmatrix_multiply_create(mwt, sd);
  sumd = _cpl_matrix_sum(mtmp);
  cpl_matrix_delete(mtmp);

  mtmp = muse_cplmatrix_multiply_create(mwt, msgxy);
  cpl_matrix_multiply(mtmp, sd);
  sumgd = _cpl_matrix_sum(mtmp);
  cpl_matrix_delete(mtmp);

  mtmp = muse_cplmatrix_multiply_create(mwt, sd);
  cpl_matrix_multiply(mtmp, mdg);
  sddg = _cpl_matrix_sum(mtmp);
  cpl_matrix_delete(mtmp);
  cpl_matrix_delete(sd);

  /* Compute height of the best-fitting marginal Gaussian. If this is not *
   * greater than 0, the centroid makes no sense                          */
  double height = (sumgd - sumg * sumd / p) / (sumgsqr - (sumg * sumg) / p);
  if (height <= 0.) {
    return 1;
  }

  double skylvl = (sumd - height * sumg) / p;
  double _offset = (sgdg - (sddg - sdg * (height * sumg + skylvl * p))) /
      (height * sdgs / sigsqr);

  if (isnan(_offset)) {
    return 2;
  }

  *offset =_offset;
  return 0;
}


/* Type independent shortcut for numerous computations of the square of *
 * a value in the following function.                                   */
#define MUSE_SQR(x)  ((x) * (x))

/*----------------------------------------------------------------------------*/
/**
  @brief   Find positive brightness perturbations (i.e stars) in an image.
  @param   aImage        2D image to be searched for stars.
  @param   aHmin         Minimum value above background for threshold detection
  @param   aFwhm         FWHM (in pixels) to be used in the convolution filter
  @param   aSharpLimits  2 element vector giving low and high cutoff for the
                         sharpness statistic.
  @param   aRoundLimits  2 element vector giving low and high cutoff for the
                         roundness statistic.
  @return  On success, a table containing the pixel coordinates and the
           measured properties of the detected sources is returned, or @c NULL
           in case of an error.

 Find positive brightness perturbations (i.e stars) in an image and
 return the centroid position, (uncalibrated) flux and the shape parameters
 (roundness and sharpness).

 For each detected star the measured properties are stored in one row of the
 results table. The measured coordinates, flux, sharpness and roundness are
 written to the table columns @c X, @c Y, @c Flux, @c Sharpness, and
 @c Roundness, respectively.

 The size of the input image @em aImage must be even along its x-axis. The
 minimum value above the background @em aHmin should, in general, be set to
 3 or 4 sigma above background RMS. The input FWHM @em aFwhm must be larger
 than 0.5 pixels.

 If @c NULL is passed for @em aSharpLimits and/or @em aRoundLimits, the
 built-in defaults are used.

 The default ranges for @em aSharpLimits and/or @em aRoundLimits are [0.2,1.0]
 and [-1.0,1.0] respectively. The limits of the sharpness statistic should only
 be changed if the stars have a significantly smaller or larger concentration
 than a Gaussian. Similarly, the limits of the roundness statistic should only
 be changed if the stars are significantly elongated.

 Adapted from IDL implementation of the DAOPHOT FIND routine which
 does not allow for bad pixels and uses a slightly different centroid
 algorithm. Uses marginal Gaussian fits to find centroids.

 @note
   - The sharpness statistic compares the central pixel to the mean of
     the surrounding pixels.   If this difference is greater than the
     originally estimated height of the Gaussian or less than 0.2 the height
     of the Gaussian (for the default values of @em aSharpLimits) then the
     star will be rejected.

   - use marginal Gaussian distributions to compute centroid
     As discussed in more  detail in the comments to the code, the centroid
     computation here is the same as in IRAF DAOFIND but differs slightly
     from the current DAOPHOT.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, a parameter is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         a parameter is not a valid input}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         convolution of the input image with a Gaussian failed}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         no candidate sources where found}
 */
/*----------------------------------------------------------------------------*/
cpl_table *
muse_find_stars(const cpl_image *aImage, double aHmin, double aFwhm,
                const double *aRoundLimits, const double *aSharpLimits)
{
  cpl_ensure(aImage, CPL_ERROR_NULL_INPUT, NULL);

  /* Maximum size of the convolution box in pixels */
  const int kMaxBox = 13;

  /* Default values for sharpness and roundness statistics */
  const double kSharpLimits[2] = {0.2, 1.};
  const double kRoundLimits[2] = {-1., 1.};

  cpl_size nx = cpl_image_get_size_x(aImage);
  cpl_size ny = cpl_image_get_size_y(aImage);

  cpl_ensure(nx > 0 && ny > 0, CPL_ERROR_ILLEGAL_INPUT, NULL);
  cpl_ensure(nx % 2 == 0, CPL_ERROR_ILLEGAL_INPUT, NULL);
  cpl_ensure(aFwhm >= 0.5, CPL_ERROR_ILLEGAL_INPUT, NULL);


  /* If no sharpness and/or roundness limits are given use the built-in *
   * defaults                                                           */
  if (!aRoundLimits) {
    aRoundLimits = kRoundLimits;
  }

  if (!aSharpLimits) {
    aSharpLimits = kSharpLimits;
  }

  cpl_msg_debug(__func__, "Settings: FWHM = %g, HMIN = %g, "
                "Roundness limits = [%g, %g], Sharpness limits = [%g, %g]",
                aFwhm, aHmin, aRoundLimits[0], aRoundLimits[1],
                aSharpLimits[0], aSharpLimits[1]);

  const double radius = CPL_MAX(0.637 * aFwhm, 2.001); /* radius is 1.5 sigma */
  const double radsqr = MUSE_SQR(radius);
  const double sigsqr = MUSE_SQR(aFwhm / 2.35482);

  cpl_size ipos;
  cpl_size nhalf = (cpl_size)CPL_MIN(radius, 0.5 * (kMaxBox - 1));

  /* Edge length of the convolution box in pixels */
  cpl_size nbox = 2 * nhalf + 1;

  /* Initial setup of the Gaussian convolution kernel */
  double *row2 = cpl_malloc(nbox * sizeof *row2);
  for (ipos = 0; ipos < nbox; ++ipos) {
    row2[ipos] = MUSE_SQR(ipos - nhalf);
  }

  cpl_matrix *ckernel = cpl_matrix_new(nbox, nbox);
  for (ipos = 0; ipos <= nhalf; ++ipos) {
    cpl_size jpos;
    for (jpos = 0; jpos < nbox; ++jpos) {
      register double tmp = row2[jpos] + MUSE_SQR(ipos);
      cpl_matrix_set(ckernel, nhalf - ipos, jpos, tmp);
      cpl_matrix_set(ckernel, nhalf + ipos, jpos, tmp);
    }
  }

  /* Mask identifies valid pixels in the convolution box */
  cpl_matrix *mask = cpl_matrix_new(nbox, nbox);

  for (ipos = 0; ipos < nbox; ++ipos) {
    cpl_size jpos;
    for (jpos = 0; jpos < nbox; ++jpos) {
      if (cpl_matrix_get(ckernel, ipos, jpos) > radsqr) {
        cpl_matrix_set(mask, ipos, jpos, 0.);
      }
      else {
        cpl_matrix_set(mask, ipos, jpos, 1.);
      }
    }
  }

  cpl_array *positions = muse_cplmatrix_where(mask, 0.,
                                              _muse_condition_not_equal);
  cpl_size npixels = cpl_array_get_size(positions);

  cpl_array_delete(positions);
  positions = NULL;

  /* Compute quantities for centroid computations that can be *
   * used for all stars                                       */
  cpl_matrix_multiply_scalar(ckernel, -0.5 / sigsqr);
  cpl_matrix_exponential(ckernel, CPL_MATH_E);

  /* In fitting Gaussians to the marginal sums, pixels will arbitrarily be *
   * assigned weights ranging from unity at the corners of the box to      *
   * nhalf^2 at the center (e.g. if nbox = 5 or 7, the weights will be     *
   *                                                                       *
   *                                 1   2   3   4   3   2   1             *
   *      1   2   3   2   1          2   4   6   8   6   4   2             *
   *      2   4   6   4   2          3   6   9  12   9   6   3             *
   *      3   6   9   6   3          4   8  12  16  12   8   4             *
   *      2   4   6   4   2          3   6   9  12   9   6   3             *
   *      1   2   3   2   1          2   4   6   8   6   4   2             *
   *                                 1   2   3   4   3   2   1             *
   *                                                                       *
   * respectively). This is done to desensitize the derived parameters to  *
   * possible neighboring, brighter stars.                                 */

  cpl_matrix *xwt = cpl_matrix_new(nbox, nbox);

  for (ipos = 0; ipos < nbox; ++ipos) {
    cpl_size jpos;
    double tmp = nhalf - imaxabs(ipos - nhalf) + 1;
    for(jpos = 0; jpos < nbox; ++jpos) {
      cpl_matrix_set(xwt, jpos, ipos, tmp);
    }
  }
  cpl_matrix *ywt = cpl_matrix_transpose_create(xwt);
  cpl_matrix *wt  = cpl_matrix_extract_row(xwt, 0);
  double p = _cpl_matrix_sum(wt);

  cpl_matrix *mtmp = NULL;

  mtmp = muse_cplmatrix_multiply_create(ckernel, xwt);
  cpl_matrix *sgx = _cpl_matrix_sum_rows(mtmp);
  cpl_matrix_delete(mtmp);

  /* Turn sgx into a row vector for the following computations */
  mtmp = sgx;
  sgx = cpl_matrix_transpose_create(mtmp);
  cpl_matrix_delete(mtmp);

  mtmp = muse_cplmatrix_multiply_create(ckernel, ywt);
  cpl_matrix *sgy = _cpl_matrix_sum_columns(mtmp);
  cpl_matrix_delete(mtmp);

  cpl_matrix *wsgy = muse_cplmatrix_multiply_create(wt, sgy);
  double sumgx = _cpl_matrix_sum(wsgy);

  mtmp = muse_cplmatrix_multiply_create(wsgy, sgy);
  double sumgsqy = _cpl_matrix_sum(mtmp);
  cpl_matrix_delete(mtmp);

  cpl_matrix *wsgx = muse_cplmatrix_multiply_create(wt, sgx);
  double sumgy = _cpl_matrix_sum(wsgx);

  mtmp = muse_cplmatrix_multiply_create(wsgx, sgx);
  double sumgsqx = _cpl_matrix_sum(mtmp);
  cpl_matrix_delete(mtmp);


  cpl_matrix *vec = cpl_matrix_new(1, nbox);
  for (ipos = 0; ipos < nbox; ++ipos) {
    cpl_matrix_set(vec, 0, ipos, nhalf - ipos);
  }
  cpl_matrix *dgdx = muse_cplmatrix_multiply_create(sgy, vec);
  cpl_matrix *dgdy = muse_cplmatrix_multiply_create(sgx, vec);
  cpl_matrix_delete(vec);

  cpl_matrix *wdgdx = muse_cplmatrix_multiply_create(wt, dgdx);
  cpl_matrix *wdgdy = muse_cplmatrix_multiply_create(wt, dgdy);

  mtmp = muse_cplmatrix_multiply_create(dgdx, dgdx);
  cpl_matrix *wdgdxs = muse_cplmatrix_multiply_create(wt, mtmp);
  cpl_matrix_delete(mtmp);

  mtmp = muse_cplmatrix_multiply_create(dgdy, dgdy);
  cpl_matrix *wdgdys = muse_cplmatrix_multiply_create(wt, mtmp);
  cpl_matrix_delete(mtmp);


  double sdgdx  = _cpl_matrix_sum(wdgdx);
  cpl_matrix_delete(wdgdx);
  wdgdx = NULL;

  double sdgdy  = _cpl_matrix_sum(wdgdy);
  cpl_matrix_delete(wdgdy);
  wdgdy = NULL;

  double sdgdxs = _cpl_matrix_sum(wdgdxs);
  cpl_matrix_delete(wdgdxs);
  wdgdxs = NULL;

  double sdgdys = _cpl_matrix_sum(wdgdys);
  cpl_matrix_delete(wdgdys);
  wdgdys = NULL;

  mtmp = muse_cplmatrix_multiply_create(wsgy, dgdx);
  double sgdgdx = _cpl_matrix_sum(mtmp);
  cpl_matrix_delete(mtmp);
  cpl_matrix_delete(wsgy);
  wsgy = NULL;

  mtmp = muse_cplmatrix_multiply_create(wsgx, dgdy);
  double sgdgdy = _cpl_matrix_sum(mtmp);
  cpl_matrix_delete(mtmp);
  cpl_matrix_delete(wsgx);
  wsgx = NULL;

  /* Finalize convolution kernel: Mask (i.e. set to zero) unused elements *
   * in the kernel matrix, so that they do not contribute to the sums     *
   * computed in the following.                                           *
   * Note that as a last step the mask has to be re-applied to the        *
   * convolution kernel because subtracting 'sumc' from the kernel matrix *
   * elements also alters the masked elememts so that they have to be set *
   * to zero again in order to obtain a correct convolution kernel!       */
  cpl_matrix_multiply(ckernel, mask);

  double sumc = _cpl_matrix_sum(ckernel);

  mtmp = muse_cplmatrix_multiply_create(ckernel, ckernel);
  double sumcsq = -MUSE_SQR(sumc) / npixels + _cpl_matrix_sum(mtmp);
  sumc /= npixels;
  cpl_matrix_delete(mtmp);

  cpl_matrix *c1 = cpl_matrix_new(1, nbox);
  double sumc1   = 0.;
  double sumc1sq = 0.;
  for (ipos = 0; ipos < nbox; ++ipos) {
    double tmp = exp(-0.5 * row2[ipos] / sigsqr);
    sumc1 += tmp;
    sumc1sq += MUSE_SQR(tmp);
    cpl_matrix_set(c1, 0, ipos, tmp);
  }
  sumc1 /= nbox;
  sumc1sq -= sumc1;
  cpl_free(row2);
  row2 = NULL;

  cpl_matrix_subtract_scalar(c1, sumc1);
  cpl_matrix_divide_scalar(c1, sumc1sq);

  cpl_matrix_subtract_scalar(ckernel, sumc);
  cpl_matrix_divide_scalar(ckernel, sumcsq);
  cpl_matrix_multiply(ckernel, mask);

  mtmp = muse_cplmatrix_multiply_create(ckernel, ckernel);
  cpl_msg_debug(__func__, "Relative error computed from the FWHM: %g",
                sqrt(_cpl_matrix_sum(mtmp)));
  cpl_matrix_delete(mtmp);

  /* Convolve the image with the kernel*/
  cpl_msg_debug(__func__, "Beginning convolution of image.");
  cpl_image *himage = muse_convolve_image(aImage, ckernel);

  cpl_matrix_delete(ckernel);
  ckernel = NULL;

  if (!himage) {
    cpl_matrix_delete(c1);
    cpl_matrix_delete(dgdy);
    cpl_matrix_delete(dgdx);
    cpl_matrix_delete(sgy);
    cpl_matrix_delete(sgx);
    cpl_matrix_delete(wt);
    cpl_matrix_delete(ywt);
    cpl_matrix_delete(xwt);
    cpl_matrix_delete(mask);

    cpl_error_set_message(__func__, CPL_ERROR_INCOMPATIBLE_INPUT,
                          "Convolution of the input image failed!");
    return NULL;
  }

#if DEBUG_MUSE_FINDSTARS > 0
  cpl_image_save(himage, "convolved_image_initial.fits", CPL_TYPE_DOUBLE, NULL,
                 CPL_IO_CREATE);
#endif

  /* Pad an nhalf pixel wide border with the minimum value found in the *
   * convoluted image on each side of the image.                        */
  double hmin = cpl_image_get_min(himage);

  cpl_image_fill_window(himage, 1, 1, nhalf, ny, hmin);
  cpl_image_fill_window(himage, nx - nhalf + 1, 1, nx, ny, hmin);
  cpl_image_fill_window(himage, nhalf + 1, 1, nx - nhalf, nhalf, hmin);
  cpl_image_fill_window(himage, nhalf + 1, ny - nhalf + 1, nx - nhalf, ny, hmin);

#if DEBUG_MUSE_FINDSTARS > 0
  cpl_image_save(himage, "convolved_image_padded.fits", CPL_TYPE_DOUBLE, NULL,
                 CPL_IO_CREATE);
#endif

  cpl_msg_debug(__func__, "Finished convolution of image.");

  /* From now on the central pixel will be excluded.*/
  cpl_matrix_set(mask, nhalf, nhalf, 0.);
  positions = muse_cplmatrix_where(mask, 0., _muse_condition_not_equal);
  npixels = cpl_array_get_size(positions);

  /* Array offset of valid pixels relative to the center */
  cpl_matrix *offset = cpl_matrix_new(1, npixels);
  for (ipos = 0; ipos < npixels; ++ipos) {
    cpl_matrix_set(offset, 0 , ipos,
                   cpl_array_get_cplsize(positions, ipos, NULL));
  }

  cpl_array_delete(positions);
  positions = NULL;

  cpl_matrix *xx = cpl_matrix_duplicate(offset);
  _cpl_matrix_modulo(xx, nbox);

  mtmp = cpl_matrix_duplicate(xx);
  cpl_matrix_multiply_scalar(mtmp, -1.);
  cpl_matrix_add(offset, mtmp);
  cpl_matrix_divide_scalar(offset, nbox);
  cpl_matrix_add_scalar(offset, -nhalf);
  cpl_matrix_delete(mtmp);

  cpl_matrix_add_scalar(xx, -nhalf);
  cpl_matrix_multiply_scalar(offset, nx);
  cpl_matrix_add(offset, xx);
  cpl_matrix_delete(xx);
  xx = NULL;

  /* Begin threshold dependent search */
  cpl_matrix *hmatrix = cpl_matrix_wrap(ny, nx, cpl_image_get_data_double(himage));

  /* Valid pixels are greater than or equal to aHmin */
  cpl_msg_debug(__func__, "Selecting pixels exceeding threshold value %.6g",
                aHmin);

  positions = muse_cplmatrix_where(hmatrix, aHmin, _muse_condition_greater_equal);

  cpl_size nfound = cpl_array_get_size(positions);
  if (nfound == 0) {
    cpl_array_delete(positions);
    cpl_matrix_unwrap(hmatrix);
    cpl_matrix_delete(offset);
    cpl_image_delete(himage);
    cpl_matrix_delete(c1);
    cpl_matrix_delete(dgdy);
    cpl_matrix_delete(dgdx);
    cpl_matrix_delete(sgy);
    cpl_matrix_delete(sgx);
    cpl_matrix_delete(wt);
    cpl_matrix_delete(ywt);
    cpl_matrix_delete(xwt);
    cpl_matrix_delete(mask);

    cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND,
                          "No image pixel value exceeds threshold value!");
    return NULL;
  }
  else {
    cpl_msg_debug(__func__, "Found %" CPL_SIZE_FORMAT " pixels with values "
                  "above the threshold value %.6g", nfound, aHmin);
  }

  for (ipos = 0; ipos < npixels; ++ipos) {

    /* Get values above the threshold */
    cpl_matrix *hindex = muse_cplmatrix_extract_selected(hmatrix, positions);

    cpl_array *atmp = cpl_array_duplicate(positions);
    cpl_array_add_scalar(atmp, cpl_matrix_get(offset, 0, ipos));

    cpl_matrix *hoffset = muse_cplmatrix_extract_selected(hmatrix, atmp);
    cpl_array_delete(atmp);
    atmp = NULL;

    cpl_size tsize = cpl_matrix_get_ncol(hoffset);
    if (cpl_matrix_get_ncol(hindex) < tsize) {
      tsize = cpl_matrix_get_ncol(hindex);
    }

    nfound = 0;
    cpl_size jpos = tsize;
    while (jpos--) {
      if (cpl_matrix_get(hindex, 0, jpos) < cpl_matrix_get(hoffset, 0, jpos)) {
        cpl_array_set_invalid(positions, jpos);
      }
      else {
        ++nfound;
      }

    }
    cpl_matrix_delete(hoffset);
    cpl_matrix_delete(hindex);

    if (nfound == 0) {
      cpl_array_delete(positions);
      cpl_matrix_unwrap(hmatrix);
      cpl_matrix_delete(offset);
      cpl_image_delete(himage);
      cpl_matrix_delete(c1);
      cpl_matrix_delete(dgdy);
      cpl_matrix_delete(dgdx);
      cpl_matrix_delete(sgy);
      cpl_matrix_delete(sgx);
      cpl_matrix_delete(wt);
      cpl_matrix_delete(ywt);
      cpl_matrix_delete(xwt);
      cpl_matrix_delete(mask);

      cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND,
                            "No local maximum exceeding the threshold value "
                            "was found!");
      return NULL;
    }

    muse_cplarray_erase_invalid(positions);
  }
  cpl_matrix_delete(offset);
  offset = NULL;

  cpl_table *_sources = cpl_table_new(cpl_array_get_size(positions));
#if DEBUG_MUSE_FINDSTARS > 0
  cpl_table_new_column(_sources, "xidx", CPL_TYPE_LONG_LONG);
  cpl_table_new_column(_sources, "yidx", CPL_TYPE_LONG_LONG);
#endif
  cpl_table_new_column(_sources, "X", CPL_TYPE_DOUBLE);
  cpl_table_new_column(_sources, "Y", CPL_TYPE_DOUBLE);
  cpl_table_new_column(_sources, "Flux", CPL_TYPE_DOUBLE);
  cpl_table_new_column(_sources, "Sharpness", CPL_TYPE_DOUBLE);
  cpl_table_new_column(_sources, "Roundness", CPL_TYPE_DOUBLE);

  /* Loop over the detections and compute statistics at each position */
  for (ipos = 0; ipos < cpl_table_get_nrow(_sources); ++ipos) {
    cpl_size _offset = cpl_array_get_cplsize(positions, ipos, NULL);
    cpl_size ix = _offset % nx;
    cpl_size iy = (_offset - ix) / nx;

    cpl_size imin = ix - nhalf + 1;
    cpl_size imax = ix + nhalf + 1;
    cpl_size jmin = iy - nhalf + 1;
    cpl_size jmax = iy + nhalf + 1;

#if DEBUG_MUSE_FINDSTARS > 0
    cpl_table_set_long_long(_sources, "xidx", ipos, ix);
    cpl_table_set_long_long(_sources, "yidx", ipos, iy);
#endif

    if ((imin < 1) || (jmin < 1) || (imax >= nx) || (jmax >= ny)) {
      /* Discard detection, search box does not fit within image boundaries */
      cpl_table_unselect_row(_sources, ipos);
      continue;
    }

    double flux = cpl_matrix_get(hmatrix, iy, ix);
    cpl_image *_image = cpl_image_extract(aImage, imin + 1, jmin + 1,
                                          imax + 1, jmax + 1);
    cpl_matrix *mimg = cpl_matrix_wrap(nbox, nbox,
                                       cpl_image_get_data_double(_image));

    /* Validate sharpness */
    mtmp = muse_cplmatrix_multiply_create(mask, mimg);
    double sharpness = (cpl_matrix_get(mimg, nhalf, nhalf) -
                        _cpl_matrix_sum(mtmp) / npixels) / flux;
    cpl_matrix_delete(mtmp);

    if ((sharpness < kSharpLimits[0]) || (sharpness > kSharpLimits[1])) {
      cpl_matrix_unwrap(mimg);
      cpl_image_delete(_image);

      /* Deselect detection, does not meet the sharpness criterion */
      cpl_table_unselect_row(_sources, ipos);
      continue;
    }

    /* Validate roundness */
    mtmp = _cpl_matrix_sum_columns(mimg);
    cpl_matrix_multiply(mtmp, c1);
    double dx = _cpl_matrix_sum(mtmp);
    cpl_matrix_delete(mtmp);

    cpl_matrix *_mtmp = _cpl_matrix_sum_rows(mimg);
    mtmp = cpl_matrix_transpose_create(_mtmp);
    cpl_matrix_multiply(mtmp, c1);
    double dy = _cpl_matrix_sum(mtmp);
    cpl_matrix_delete(mtmp);
    cpl_matrix_delete(_mtmp);

    if ((dx <= 0.) || (dy <= 0.)) {
      cpl_matrix_unwrap(mimg);
      cpl_image_delete(_image);

      /* Deselect detection, invalid roundness */
      cpl_table_unselect_row(_sources, ipos);
      continue;
    }

    double roundness = 2. * (dx - dy) / (dx + dy);
    if ((roundness < kRoundLimits[0]) || (roundness > kRoundLimits[1])) {
      cpl_matrix_unwrap(mimg);
      cpl_image_delete(_image);

      /* Deselect detection, does not meet the roundness criterion */
      cpl_table_unselect_row(_sources, ipos);
      continue;
    }

    /* Centroid computation: the centroid computation differs from     *
     * DAOPHOT which multiplies the correction dx by 1/(1+abs(dx)).    *
     * The DAOPHOT method is more robust (e.g. two different sources   *
     * will not merge) especially in a package where the centroid will *
     * subsequently be re-determined using PSF fitting. However, it is *
     * less accurate, and introduces biases in the centroid histogram. *
     * The change here is the same made in the IRAF DAOFIND routine    *
     * (see http://iraf.net/article.php?story=7211&query=daofind )     */

    int status = 0;

    double xoffset = 0;
    status = _muse_centroid_offset(&xoffset, 1, mimg, wt, ywt, sgy, dgdx,
                                   sumgx, sumgsqy, sgdgdx, sdgdx, sdgdxs, p,
                                   sigsqr);
    if (status != 0) {
      cpl_matrix_unwrap(mimg);
      cpl_image_delete(_image);
      cpl_table_unselect_row(_sources, ipos);
      continue;
    }

    double yoffset = 0;
    status = _muse_centroid_offset(&yoffset, 2, mimg, wt, xwt, sgx, dgdy,
                                   sumgy, sumgsqx, sgdgdy, sdgdy, sdgdys, p,
                                   sigsqr);
    if (status != 0) {
      cpl_matrix_unwrap(mimg);
      cpl_image_delete(_image);
      cpl_table_unselect_row(_sources, ipos);
      continue;
    }

    /* Bad centroid position. Deselect detection */
    if ((fabs(xoffset) >= nhalf) || (fabs(yoffset) >= nhalf)) {
      cpl_matrix_unwrap(mimg);
      cpl_image_delete(_image);
      cpl_table_unselect_row(_sources, ipos);
      continue;
    }
    cpl_matrix_unwrap(mimg);
    cpl_image_delete(_image);


    /* Compute the final centroid X and Y coordinates. Add 1 to ix and iy *
     * to convert matrix element indices to image pixel positions, since  *
     * the latter count pixels starting from 1!                           */
    double xcenter = (ix + 1) + xoffset;
    double ycenter = (iy + 1) + yoffset;

    cpl_table_set_double(_sources, "X", ipos, xcenter);
    cpl_table_set_double(_sources, "Y", ipos, ycenter);
    cpl_table_set_double(_sources, "Flux", ipos, flux);
    cpl_table_set_double(_sources, "Sharpness", ipos, sharpness);
    cpl_table_set_double(_sources, "Roundness", ipos, roundness);
  }

  /* Cleanup */
  cpl_array_delete(positions);
  cpl_matrix_unwrap(hmatrix);
  cpl_image_delete(himage);
  cpl_matrix_delete(c1);
  cpl_matrix_delete(dgdy);
  cpl_matrix_delete(dgdx);
  cpl_matrix_delete(sgy);
  cpl_matrix_delete(sgx);
  cpl_matrix_delete(wt);
  cpl_matrix_delete(ywt);
  cpl_matrix_delete(xwt);
  cpl_matrix_delete(mask);


#if DEBUG_MUSE_FINDSTARS > 0
  cpl_table_save(_sources, NULL, NULL, "detections.fits", CPL_IO_CREATE);
#endif

  cpl_table *sources =  cpl_table_extract_selected(_sources);
  cpl_size ncandidates = cpl_table_get_nrow(_sources);
  cpl_size nselected   = cpl_table_get_nrow(sources);

  cpl_table_delete(_sources);

#if DEBUG_MUSE_FINDSTARS > 0
  cpl_table_erase_column(sources, "xidx");
  cpl_table_erase_column(sources, "yidx");
#endif

  if (ncandidates == 0) {
    cpl_msg_debug(__func__, "No candidate stars were found!");
  }
  else {
    cpl_msg_debug(__func__, "Total number of candidate stars %" CPL_SIZE_FORMAT
                  "; no. selected candidates %" CPL_SIZE_FORMAT, ncandidates,
                  nselected);
  }

  if (nselected == 0) {
    cpl_table_delete(sources);
    sources = NULL;
  }

  return sources;
}
#undef MUSE_SQR

/**@}*/
