/* $Id: naco_img_jitter.c,v 1.123 2011-12-22 11:09:36 llundin Exp $
 *
 * This file is part of the NACO Pipeline
 * Copyright (C) 2002,2003 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 St, Fifth Floor, Boston, MA  02111-1307  USA
 */

/*
 * $Author: llundin $
 * $Date: 2011-12-22 11:09:36 $
 * $Revision: 1.123 $
 * $Name: not supported by cvs2svn $
 */

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

/*-----------------------------------------------------------------------------
                                Includes
 -----------------------------------------------------------------------------*/

#include "naco_recipe.h"
#include "naco_strehl.h"

#include "irplib_strehl.h"
#include "irplib_calib.h"

/* Needed for memmove() */
#include <string.h>

/*-----------------------------------------------------------------------------
                            Defines
 -----------------------------------------------------------------------------*/

#define RECIPE_STRING "naco_img_jitter"

#define EXT0 0

#define NACO_MIN(A,B) ((A) < (B) ? (A) : (B))
#define NACO_MAX(A,B) ((A) > (B) ? (A) : (B))

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

#ifndef NACO_IMG_JITTER_KEEP_SKY_OBJECTS
static cpl_error_code naco_img_jitter_reject_objects(cpl_image *, double,
                                                     double);
#endif

static cpl_error_code naco_img_jitter_find_strehl(const cpl_imagelist *,
                                                  const cpl_parameterlist *,
                                                  const irplib_framelist *);

static cpl_image * naco_img_jitter_saa_center(const cpl_imagelist *,
                                              cpl_bivector *);

static cpl_image * naco_img_jitter_saa_lucky(const cpl_imagelist *,
                                             const cpl_vector *,
                                             const cpl_bivector *, double);

static cpl_image * naco_img_jitter_find_sky_cube(int, int, const cpl_array *,
                                                 const irplib_framelist *);

static cpl_error_code naco_img_jitter_imagelist_wrap_nocube(cpl_imagelist *,
                                                            const cpl_array *,
                                                            cpl_imagelist *);


static cpl_image * naco_img_jitter_combine_cube(cpl_table *, const cpl_imagelist *,
                                                const cpl_propertylist *,
                                                const cpl_parameterlist *, int);

static cpl_error_code naco_img_jitter_check_cube(const cpl_imagelist *,
                                                 const cpl_image *);

static cpl_error_code naco_img_jitter_do_cube(cpl_imagelist *, cpl_table *,
                                              cpl_propertylist *,
                                              const cpl_array *,
                                              const irplib_framelist *,
                                              const cpl_parameterlist *);

static
cpl_error_code naco_img_jitter_load_objimages(cpl_imagelist *, cpl_array *,
                                              const irplib_framelist *,
                                              const cpl_parameterlist *);

static cpl_image ** naco_img_jitter_reduce(cpl_imagelist *, cpl_table *,
                                           cpl_propertylist *,
                                           const irplib_framelist *,
                                           const irplib_framelist *,
                                           const cpl_parameterlist *,
                                           const char *,
                                           const char *,
                                           const char *,
                                           cpl_boolean *);

static cpl_image * naco_img_jitter_find_sky(cpl_propertylist *,
                                            const cpl_imagelist *,
                                            const cpl_imagelist *);
static cpl_image ** naco_img_jitter_saa(cpl_imagelist *,
                                        const irplib_framelist *,
                                        const cpl_parameterlist *);
static cpl_error_code naco_img_jitter_qc(cpl_propertylist *, cpl_propertylist *,
                                         const irplib_framelist *,
                                         const cpl_image *, cpl_boolean);
static cpl_error_code naco_img_jitter_qc_apertures(cpl_propertylist *,
                                                   const irplib_framelist *,
                                                   const cpl_image *);
static cpl_error_code naco_img_jitter_save(cpl_frameset *,
                                           const cpl_parameterlist *,
                                           const cpl_propertylist *,
                                           const cpl_propertylist *,
                                           const cpl_imagelist *,
                                           const cpl_image *,
                                           const cpl_image *,
                                           const cpl_table *);

NACO_RECIPE_DEFINE(naco_img_jitter, 
                   NACO_PARAM_OFFSETS |
                   NACO_PARAM_OBJECTS |
                   NACO_PARAM_ODDEVEN |
                   NACO_PARAM_XCORR   |
                   NACO_PARAM_UNION   |
                   NACO_PARAM_COMBINE |
                   NACO_PARAM_SKYPLANE|
                   NACO_PARAM_CUBEMODE|
                   NACO_PARAM_LUCK_STR|
                   NACO_PARAM_SAVECUBE|
                   NACO_PARAM_REJ_HILO,
                   "Jitter recipe",
                  RECIPE_STRING " -- NACO imaging jitter recipe.\n"         
                  "The files listed in the Set Of Frames (sof-file) "
                  "must be tagged:\n" 
                  "NACO-raw-file.fits "NACO_IMG_JITTER_OBJ" or\n"            
                  "NACO-raw-file.fits "NACO_IMG_JITTER_SKY" or\n"            
                  "NACO-raw-file.fits "NACO_IMG_JITTER_OBJ_POL" or\n"        
                  "NACO-raw-file.fits "NACO_IMG_JITTER_SKY_POL" or\n"        
                  "NACO-flat-file.fits "NACO_CALIB_FLAT" or\n"               
                  "NACO-bpm-file.fits "NACO_CALIB_BPM" or\n"               
                  "NACO-dark-file.fits "NACO_CALIB_DARK"\n");

/*----------------------------------------------------------------------------*/
/**
 * @defgroup naco_img_jitter   Jitter
 */
/*----------------------------------------------------------------------------*/

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

/*----------------------------------------------------------------------------*/
/**
  @brief    Get the command line options and execute the data reduction
  @param    framelist   the frames list
  @param    parlist     the parameters list
  @return   0 iff everything is ok
 */
/*----------------------------------------------------------------------------*/
static int naco_img_jitter(cpl_frameset            * framelist,
                           const cpl_parameterlist * parlist)
{
    cpl_errorstate cleanstate = cpl_errorstate_get();
    irplib_framelist * allframes = NULL;
    irplib_framelist * objframes = NULL;
    irplib_framelist * skyframes = NULL;
    cpl_propertylist * qclist    = cpl_propertylist_new();
    cpl_propertylist * paflist   = cpl_propertylist_new();
    cpl_imagelist    * objimages = cpl_imagelist_new();
    cpl_image       ** combined  = NULL;
    cpl_table        * cubetable = cpl_table_new(1);
    const char       * badpix;
    const char       * dark;
    const char       * flat;
    cpl_boolean        drop_wcs = CPL_FALSE;


    /* Identify the RAW and CALIB frames in the input frameset */
    skip_if (naco_dfs_set_groups(framelist));

    allframes = irplib_framelist_cast(framelist);
    skip_if(allframes == NULL);

    objframes = irplib_framelist_extract_regexp(allframes,
                                                "^(" NACO_IMG_JITTER_OBJ "|"
                                                NACO_IMG_JITTER_OBJ_POL ")$",
                                                CPL_FALSE);
    skip_if(objframes == NULL);

    skyframes = irplib_framelist_extract_regexp(allframes,
                                                "^(" NACO_IMG_JITTER_SKY "|"
                                                NACO_IMG_JITTER_SKY_POL ")$",
                                                CPL_FALSE);
    irplib_framelist_empty(allframes);
    if (skyframes == NULL) {
        naco_error_reset("The set of frames has no sky frames:");
    }

    flat = irplib_frameset_find_file(framelist, NACO_CALIB_FLAT);
    bug_if(0);

    badpix = irplib_frameset_find_file(framelist, NACO_CALIB_BPM);
    bug_if(0);

    dark = irplib_frameset_find_file(framelist, NACO_CALIB_DARK);
    bug_if(0);

    skip_if(irplib_framelist_load_propertylist(objframes, 0, 0, "^("
                                               NACO_PFITS_REGEXP_JITTER_FIRST
                                               ")$", CPL_FALSE));

    skip_if(irplib_framelist_load_propertylist_all(objframes, 0, "^("
                                                   NACO_PFITS_REGEXP_JITTER_ALL
                                                   ")$", CPL_FALSE));

    /* Apply the reduction */
    cpl_msg_info(cpl_func, "Apply the data recombination");
    combined = naco_img_jitter_reduce(objimages, cubetable, qclist, objframes,
                                      skyframes, parlist, dark, flat, badpix,
                                      &drop_wcs);

    skip_if (combined == NULL);

    irplib_framelist_empty(skyframes);

    /* Compute QC parameters from the combined image */
    cpl_msg_info(cpl_func, "Compute QC parameters from the combined image");
    skip_if (naco_img_jitter_qc(qclist, paflist, objframes, combined[0], drop_wcs));

    irplib_framelist_empty(objframes);
    
    /* Save the products */
    cpl_msg_info(cpl_func, "Save the products");

    /* PRO.CATG */
    bug_if (cpl_propertylist_append_string(paflist, CPL_DFS_PRO_CATG,
                                           cpl_table_get_ncol(cubetable) > 0 ?
                                           NACO_IMG_JITTER_CUBE :
                                           NACO_IMG_JITTER_COMB));

    skip_if (naco_img_jitter_save(framelist, parlist, qclist, paflist,
                                  objimages,
                                  combined[0], combined[1], cubetable));

    end_skip;
    
    if (combined != NULL) {
        cpl_image_delete(combined[0]);
        cpl_image_delete(combined[1]);
        cpl_free(combined);
    }
    cpl_imagelist_delete(objimages);
    irplib_framelist_delete(allframes);
    irplib_framelist_delete(objframes);
    irplib_framelist_delete(skyframes);
    cpl_propertylist_delete(qclist);
    cpl_propertylist_delete(paflist);
    cpl_table_delete(cubetable);

    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief The recipe data reduction part is implemented here 
  @param objimages   Object images
  @param cubetable   Table
  @param qclist      List of QC parameters
  @param obj         the objects frames
  @param sky         the sky frames or NULL
  @param parlist     the parameters list
  @param dark        the dark or NULL
  @param flat        the flat field or NULL
  @param bpm         the bad pixels map or NULL
  @param pdid_resize Set to true if the combined image was resized
  @return   the combined image pair and the contribution map or NULL on error
 */
/*----------------------------------------------------------------------------*/
static cpl_image ** naco_img_jitter_reduce(cpl_imagelist          * objimages,
                                           cpl_table              * cubetable,
                                           cpl_propertylist       * qclist,
                                           const irplib_framelist * obj,
                                           const irplib_framelist * sky,
                                           const cpl_parameterlist * parlist,
                                           const char             * dark,
                                           const char             * flat,
                                           const char             * bpm,
                                           cpl_boolean            * pdid_resize)
{
    cpl_errorstate  prestate = cpl_errorstate_get();
    const int        nobj = irplib_framelist_get_size(obj);
    cpl_imagelist  * nocubeimgs = NULL;
    cpl_imagelist  * skyimages  = NULL;
    cpl_image      * skyimage   = NULL;
    cpl_image     ** combined   = NULL;
    cpl_array      * iscube     = NULL;
    cpl_boolean      has_cube   = CPL_FALSE;
    const char     * scubemode = naco_parameterlist_get_string
        (parlist, RECIPE_STRING, NACO_PARAM_CUBEMODE);
    int              inocube    = 0;


    bug_if(pdid_resize == NULL);
    bug_if(scubemode == NULL);

    skip_if (irplib_framelist_contains(obj, NACO_PFITS_DOUBLE_CUMOFFSETX,
                                       CPL_TYPE_DOUBLE, CPL_FALSE, 0.0));

    skip_if (irplib_framelist_contains(obj, NACO_PFITS_DOUBLE_CUMOFFSETY,
                                       CPL_TYPE_DOUBLE, CPL_FALSE, 0.0));

    /* Load the input data */
    cpl_msg_info(cpl_func, "Loading the %d object and %d sky images",
                 irplib_framelist_get_size(obj),
                 sky == NULL ? 0 : irplib_framelist_get_size(sky));

    iscube = cpl_array_new(nobj, CPL_TYPE_INT);

    (void)naco_img_jitter_load_objimages(objimages, iscube, obj, parlist);

    any_if("Could not load the %d object images", nobj);

    cpl_msg_info(cpl_func, "Loaded %d object images", nobj);

    skip_if(cpl_imagelist_get_size(objimages) != nobj);

    skip_if (irplib_flat_dark_bpm_calib(objimages, flat, dark, bpm));

    cpl_msg_info(cpl_func, "Sky estimation and correction");

    if (scubemode[0] == 'a') {
        /* Just add without shifting */
        const int mcube = nobj - cpl_array_count_invalid(iscube);

        if (mcube > 0) {
            cpl_msg_info(cpl_func, "Collapsing %d cube(s) with no shifting",
                         mcube);
            cpl_array_delete(iscube);
            iscube = cpl_array_new(nobj, CPL_TYPE_INT);
        }
    }

    if (cpl_array_has_valid(iscube)) {
        has_cube = CPL_TRUE;

        skip_if(naco_img_jitter_do_cube(objimages, cubetable, qclist,
                                        iscube, obj, parlist));

#ifdef NACO_IMG_JITTER_DEVELOPMENT
        skip_if(cpl_imagelist_save(objimages, "Collapsed.fits", CPL_BPP_IEEE_FLOAT,
                                   NULL, CPL_IO_CREATE));
#endif
    }

    if (cpl_array_has_invalid(iscube)) {

        nocubeimgs = has_cube ? cpl_imagelist_new() : objimages;
        if (has_cube) {
            skip_if(naco_img_jitter_imagelist_wrap_nocube(nocubeimgs, iscube,
                                                          objimages));
            inocube = cpl_imagelist_get_size(nocubeimgs);
        }

        /* Estimate the sky */
        if (sky != NULL) {
            skyimages = irplib_imagelist_load_framelist(sky, CPL_TYPE_FLOAT, 0, 0);
            any_if("Could not load sky images");
            skip_if (irplib_flat_dark_bpm_calib(skyimages, flat, dark, bpm));
        }

        skyimage = naco_img_jitter_find_sky(qclist, nocubeimgs, skyimages);
        any_if("Could not estimate sky");

        cpl_imagelist_delete(skyimages);
        skyimages = NULL;

        /* Apply the sky correction */
        skip_if (cpl_imagelist_subtract_image(nocubeimgs, skyimage));
    }

    /* Find the Strehl ratio of all object frames */
    if(naco_img_jitter_find_strehl(objimages, parlist, obj)) {
        irplib_error_recover(prestate, "Could not compute Strehl-ratio "
                             "for %d frame(s)", nobj);
    }

    /* Apply the shift and add */
    cpl_msg_info(cpl_func, "Shift and add");
    combined = naco_img_jitter_saa(objimages, obj, parlist);
    skip_if (combined == NULL);

    *pdid_resize = (cpl_boolean)
        (cpl_image_get_size_x(combined[0])
         != cpl_image_get_size_x(cpl_imagelist_get_const(objimages, 0)) ||
         cpl_image_get_size_y(combined[0])
         != cpl_image_get_size_y(cpl_imagelist_get_const(objimages, 0)));

    end_skip;

    /* Unwrap the wrapped no-cube images */
    for (;inocube > 0;) {
        (void)cpl_imagelist_unset(nocubeimgs, --inocube);
    }

    if (nocubeimgs != objimages) cpl_imagelist_delete(nocubeimgs);
    cpl_imagelist_delete(skyimages);
    cpl_image_delete(skyimage);
    cpl_array_delete(iscube);

    return combined;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Estimate the sky
  @param    qclist  List of QC parameters
  @param    objs    the images with objects
  @param    skys    the images with the sky or NULL
  @return   The sky image on NULL on error
 */
/*----------------------------------------------------------------------------*/
static cpl_image * naco_img_jitter_find_sky(cpl_propertylist    * qclist,
                                            const cpl_imagelist * objs,
                                            const cpl_imagelist * skys)
{
    cpl_image * self = NULL;

    if (skys != NULL) {
        /* Use sky images */
        self = cpl_imagelist_collapse_median_create(skys);
        any_if("Could not compute the median of %d sky images",
               (int)cpl_imagelist_get_size(skys));
    } else {
        /* Use objects images */
        /* FIXME: This will work badly for few (one!) images */
        self = cpl_imagelist_collapse_median_create(objs);
        any_if("Could not compute the median of %d object images",
               (int)cpl_imagelist_get_size(objs));
    }

    /* Get the background value */
    bug_if(cpl_propertylist_append_double(qclist, "ESO QC BACKGD",
                                          cpl_image_get_median(self)));
    bug_if(cpl_propertylist_append_double(qclist, "ESO QC BACKGD STDEV",
                                          cpl_image_get_stdev(self)));

    end_skip;
    
    return self;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Apply the shift and add
  @param    imlist      the images to stack
  @param    objframes   the list of objects frames
  @param    parlist     the parameters list
  @return   the combined image pair and the contribution map or NULL on error
 */
/*----------------------------------------------------------------------------*/
static cpl_image ** naco_img_jitter_saa(cpl_imagelist    * imlist,
                                        const irplib_framelist * objframes,
                                        const cpl_parameterlist * parlist)
{
    const char   * sval;
    cpl_bivector * offsets_est = NULL;
    cpl_bivector * objs = NULL;
    cpl_image   ** combined = NULL;
    cpl_vector   * sigmas = NULL;
    double         psigmas[] = {5.0, 2.0, 1.0, 0.5};
    const char   * offsets;
    const char   * objects;
#ifdef NACO_USE_ODDEVEN
    cpl_boolean    oddeven_flag;
#endif
    const int      nsigmas = (int)(sizeof(psigmas)/sizeof(double));
    const int      nfiles = cpl_imagelist_get_size(imlist);
    int            sx, sy, mx, my;
    int            rej_low, rej_high;
    const char   * combine_string;
    cpl_geom_combine combine_mode;


    bug_if(0);
    bug_if (irplib_framelist_get_size(objframes) != nfiles);

    /* Retrieve input parameters */

    /* Offsets */
    offsets = naco_parameterlist_get_string(parlist, RECIPE_STRING,
                                            NACO_PARAM_OFFSETS);
    /* Objects */
    objects = naco_parameterlist_get_string(parlist, RECIPE_STRING,
                                            NACO_PARAM_OBJECTS);
#ifdef NACO_USE_ODDEVEN
    /* Oddeven flag */
    oddeven_flag = naco_parameterlist_get_bool(parlist, RECIPE_STRING,
                                               NACO_PARAM_ODDEVEN);
#endif

    /* Cross correlation windows parameters */
    sval = naco_parameterlist_get_string(parlist, RECIPE_STRING,
                                      NACO_PARAM_XCORR);
    bug_if (sval == NULL);

    skip_if (sscanf(sval, "%d %d %d %d", &sx, &sy, &mx, &my) != 4);

    combine_string = naco_parameterlist_get_string(parlist, RECIPE_STRING,
                                                   NACO_PARAM_COMBINE);

    bug_if (combine_string == NULL);

    if (combine_string[0] == 'u')
        combine_mode = CPL_GEOM_UNION;
    else if (combine_string[0] == 'f')
        combine_mode = CPL_GEOM_FIRST;
    else if (combine_string[0] == 'i')
        combine_mode = CPL_GEOM_INTERSECT;
    else
        skip_if(1);

    /* Number of rejected values in stacking */
    sval = naco_parameterlist_get_string(parlist, RECIPE_STRING,
                                      NACO_PARAM_REJ_HILO);
    bug_if (sval == NULL);

    skip_if (sscanf(sval, "%d %d", &rej_low, &rej_high) != 2);

    
    /* Get the offsets estimation of each input file pair */
    cpl_msg_info(cpl_func, "Get the offsets estimation");
    offsets_est = NULL;
    if (offsets &&
            offsets[0] != (char)0) {
        /* A file has been provided on the command line */
        offsets_est = cpl_bivector_read(offsets);
        if (offsets_est == NULL ||
            cpl_bivector_get_size(offsets_est) != nfiles) {
            cpl_msg_error(cpl_func, "Cannot get offsets from %s", 
                    offsets);
            skip_if(1);
        }
    } else {
        double * offsets_est_x;
        double * offsets_est_y;
        double offx0 = DBL_MAX; /* Avoid (false) uninit warning */
        double offy0 = DBL_MAX; /* Avoid (false) uninit warning */
        int i;

        /* Get the offsets from the header */
        offsets_est = cpl_bivector_new(nfiles);
        offsets_est_x = cpl_bivector_get_x_data(offsets_est);
        offsets_est_y = cpl_bivector_get_y_data(offsets_est);
        for (i=0 ; i < nfiles ; i++) {
            const cpl_propertylist * plist
                = irplib_framelist_get_propertylist_const(objframes, i);

            /* X and Y offsets */
            if (i == 0) {
                offx0 = naco_pfits_get_cumoffsetx(plist);
                offy0 = naco_pfits_get_cumoffsety(plist);
            }

            /* Subtract the first offset to all offsets */
            offsets_est_x[i] = offx0 - naco_pfits_get_cumoffsetx(plist);
            offsets_est_y[i] = offy0 - naco_pfits_get_cumoffsety(plist);

            bug_if(0);
        }
    }

    /* Read the provided objects file if provided */
    if (objects &&
            objects[0] != (char)0) {
        cpl_msg_info(cpl_func, "Get the user provided correlation objects");
        /* A file has been provided on the command line */
        objs = cpl_bivector_read(objects);
        if (objs==NULL) {
            cpl_msg_error(cpl_func, "Cannot get objects from %s",
                    objects);
            skip_if (1);
        }
    }

    /* Create the vector for the detection thresholds */
    sigmas = cpl_vector_wrap(nsigmas, psigmas);
    
    /* Recombine the images */
    cpl_msg_info(cpl_func, "Recombine the images set");
    combined = cpl_geom_img_offset_combine(imlist, offsets_est, 1, objs,
                                           sigmas, NULL, sx, sy, mx, my,
                                           rej_low, rej_high, combine_mode);

    skip_if (combined == NULL);

    end_skip;

    cpl_bivector_delete(offsets_est);
    cpl_bivector_delete(objs);
    cpl_vector_unwrap(sigmas);

    return combined;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Compute some QC parameters from the combined image
  @param    qclist    List of QC parameters
  @param    paflist   List of PAF parameters
  @param    objframes the list of objects frames
  @param    combined  the combined image produced
  @param    drop_wcs  Drop any WCS keys iff true
  @return   0 iff everything is ok
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_img_jitter_qc(cpl_propertylist       * qclist,
                                         cpl_propertylist       * paflist,
                                         const irplib_framelist * objframes,
                                         const cpl_image        * combined,
                                         cpl_boolean              drop_wcs)
{
    cpl_errorstate cleanstate = cpl_errorstate_get();
    const cpl_propertylist * reflist
        = irplib_framelist_get_propertylist_const(objframes, 0);
    const char    * sval;


    bug_if(combined == NULL);

    bug_if(cpl_propertylist_copy_property_regexp(paflist, reflist, "^("
                                                 NACO_PFITS_REGEXP_JITTER_PAF
                                                 ")$", 0));

    if (naco_img_jitter_qc_apertures(qclist, objframes, combined)) {
        naco_error_reset("Could not compute all QC parameters");
    }

    sval = naco_pfits_get_filter(reflist);
    if (cpl_error_get_code()) {
        naco_error_reset("Could not get FITS key:");
    } else {
        cpl_propertylist_append_string(qclist, "ESO QC FILTER OBS", sval);
    }
    sval = naco_pfits_get_opti3_name(reflist);
    if (cpl_error_get_code()) {
        naco_error_reset("Could not get FITS key:");
    } else {
        cpl_propertylist_append_string(qclist, "ESO QC FILTER NDENS", sval);
    }
    sval = naco_pfits_get_opti4_name(reflist);
    if (cpl_error_get_code()) {
        naco_error_reset("Could not get FITS key:");
    } else {
        cpl_propertylist_append_string(qclist, "ESO QC FILTER POL", sval);
    }

    bug_if(0);

    bug_if (cpl_propertylist_append(paflist, qclist));

    if (drop_wcs) {
        cpl_propertylist * pcopy = cpl_propertylist_new();
        const cpl_error_code error
            = cpl_propertylist_copy_property_regexp(pcopy, reflist, "^("
                                                    IRPLIB_PFITS_WCS_REGEXP
                                                    ")$", 0);
        if (!error && cpl_propertylist_get_size(pcopy) > 0) {
            cpl_msg_warning(cpl_func, "Combined image will have no WCS "
                            "coordinates");
        }
        cpl_propertylist_delete(pcopy);

        bug_if (cpl_propertylist_copy_property_regexp(qclist, reflist, "^("
                                                      NACO_PFITS_REGEXP_JITTER_COPY
                                                      ")$", 0));
    } else {
        bug_if(cpl_propertylist_copy_property_regexp(qclist, reflist, "^("
                                                     IRPLIB_PFITS_WCS_REGEXP "|"
                                                     NACO_PFITS_REGEXP_JITTER_COPY
                                                     ")$", 0));
    }

    if (cpl_propertylist_has(qclist, "AIRMASS"))
        bug_if (irplib_pfits_set_airmass(qclist, objframes));

    end_skip;

    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Compute some QC parameters from the combined image
  @param    qclist    List of QC parameters
  @param    objframes the list of objects frames
  @param    combined  the combined image produced
  @return   0 iff everything is ok
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_img_jitter_qc_apertures(cpl_propertylist * qclist,
                                                   const irplib_framelist *
                                                   objframes,
                                                   const cpl_image  * combined)
{
    const cpl_propertylist * reflist
        = irplib_framelist_get_propertylist_const(objframes, 0);

    cpl_apertures * aperts = NULL;
    cpl_bivector  * fwhms  = NULL;
    cpl_vector    * fwhms_sum = NULL;
    cpl_vector    * sigmas = NULL;
    double        * fwhms_x;
    double        * fwhms_y;
    double        * fwhms_sum_data;
    int             nb_val, nb_good;
    double          f_min, f_max;
    double          psigmas[] = {5.0, 2.0, 1.0, 0.5}; /* Not modified */
    const int       nsigmas = (int)(sizeof(psigmas)/sizeof(double));
    cpl_size        isigma;
    int             i;

    const double    pixscale = naco_pfits_get_pixscale(reflist);
    /* FIXME: Some hard-coded constants */
    const double    seeing_min_arcsec = 0.1;
    const double    seeing_max_arcsec = 5.0;
    const double    seeing_fwhm_var   = 0.2;

    double iq, fwhm_pix, fwhm_arcsec;


    /* Detect apertures */
    cpl_msg_info(cpl_func, "Detecting apertures using %d sigma-levels "
                 "ranging from %g down to %g", nsigmas, psigmas[0],
                 psigmas[nsigmas-1]);

    /* pixscale could be mising */
    skip_if( pixscale <= 0.0 );
    bug_if( seeing_min_arcsec < 0.0);

    /* Create the vector for the detection thresholds. (Not modified) */
    sigmas = cpl_vector_wrap(nsigmas, psigmas);

    aperts = cpl_apertures_extract(combined, sigmas, &isigma);
    if (aperts == NULL) {
        cpl_msg_warning(cpl_func, "Could not detect any apertures in combined "
                        "image");
        skip_if(1);
    }
    bug_if(0);

    /* Compute the FHWM of the detected apertures */
    fwhms = cpl_apertures_get_fwhm(combined, aperts);
    if (fwhms == NULL) {
        cpl_msg_warning(cpl_func, "Could not compute any FWHMs of the "
                          "apertures in the combined image");
        skip_if(1);
    }
    bug_if(0);
    cpl_apertures_delete(aperts);
    aperts = NULL;

    /* Access the data */
    nb_val  = cpl_bivector_get_size(fwhms);
    fwhms_x = cpl_bivector_get_x_data(fwhms);
    fwhms_y = cpl_bivector_get_y_data(fwhms);

    /* Get the number of good values */
    nb_good = 0;
    for (i=0 ; i < nb_val ; i++) {
        if (fwhms_x[i] <= 0.0 || fwhms_y[i] <= 0.0) continue;
        fwhms_x[nb_good] = fwhms_x[i];
        fwhms_y[nb_good] = fwhms_y[i];
        nb_good++;
    }

    cpl_msg_info(cpl_func, "Detected %d (%d) apertures at sigma=%g",
                 nb_good, nb_val, psigmas[isigma]);

    skip_if_lt (nb_good, 1, "aperture with a FWHM");

    nb_val = nb_good;
    /* This resizing is not really needed */
    bug_if(cpl_vector_set_size(cpl_bivector_get_x(fwhms), nb_val));
    bug_if(cpl_vector_set_size(cpl_bivector_get_y(fwhms), nb_val));
    fwhms_x = cpl_bivector_get_x_data(fwhms);
    fwhms_y = cpl_bivector_get_y_data(fwhms);

    /* Get the good values */
    fwhms_sum = cpl_vector_new(nb_good);
    fwhms_sum_data = cpl_vector_get_data(fwhms_sum);
    for (i=0; i < nb_good; i++) {
        fwhms_sum_data[i] = fwhms_x[i] + fwhms_y[i];
    }
    /* Compute the fwhm as the median of the average of the FHWMs in x and y */
    fwhm_pix = 0.5 * cpl_vector_get_median(fwhms_sum);

    bug_if(cpl_propertylist_append_double(qclist, "ESO QC FWHM PIX", fwhm_pix));

    fwhm_arcsec = fwhm_pix * pixscale;

    bug_if(cpl_propertylist_append_double(qclist, "ESO QC FWHM ARCSEC",
                                          fwhm_arcsec));

    /* iq is the median of the (fwhm_x+fwhm_y)/2 */
    /* of the good stars */

    /* Compute f_min and f_max */
    f_min = seeing_min_arcsec / pixscale;
    f_max = seeing_max_arcsec / pixscale;

    /* Sum the the good values */
    nb_good = 0;
    for (i=0 ; i < nb_val ; i++) {
        const double fx = fwhms_x[i];
        const double fy = fwhms_y[i];

        if (fx <= f_min || f_max <= fx || fy <= f_min || f_max <= fy) continue;
        if (fabs(fx-fy) >= 0.5 * (fx + fy) * seeing_fwhm_var) continue;

        fwhms_sum_data[nb_good] = fx + fy;
        nb_good++;
    }

    cpl_msg_info(cpl_func, "%d of the apertures have FWHMs within the range "
                 "%g to %g and eccentricity within %g", nb_good, f_min, f_max,
                 seeing_fwhm_var);

    skip_if_lt (nb_good, 1, "aperture with a good FWHM");

    bug_if(cpl_vector_set_size(fwhms_sum, nb_good));

    /* Compute the fwhm */
    iq = pixscale * 0.5 * cpl_vector_get_median(fwhms_sum);

    bug_if(cpl_propertylist_append_double(qclist, "ESO QC IQ", iq));

    end_skip;

    cpl_vector_delete(fwhms_sum);
    cpl_bivector_delete(fwhms);
    cpl_vector_unwrap(sigmas);
    cpl_apertures_delete(aperts);

    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Save the jitter recipe products on disk
  @param    set         the input frame set
  @param    parlist     the input list of parameters
  @param    qclist      List of QC parameters
  @param    paflist     List of PAF parameters
  @param    objimages   The objimages to be appended to product
  @param    combined    the combined image produced
  @param    contrib     the contribution map of the combined image
  @param    cubetable   Table of cube combination data
  @return   0 iff everything is ok
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_img_jitter_save(cpl_frameset            * set,
                                           const cpl_parameterlist * parlist,
                                           const cpl_propertylist  * qclist,
                                           const cpl_propertylist  * paflist,
                                           const cpl_imagelist     * objimages,
                                           const cpl_image         * combined,
                                           const cpl_image         * contrib,
                                           const cpl_table         * cubetable)
{

    const cpl_boolean is_cube = cpl_table_get_ncol(cubetable) > 0
        ? CPL_TRUE : CPL_FALSE;
    const char * procatg =  is_cube ? NACO_IMG_JITTER_CUBE
        : NACO_IMG_JITTER_COMB;
    cpl_propertylist * xtlist = cpl_propertylist_new();
    const cpl_boolean save_cube =
        naco_parameterlist_get_bool(parlist, RECIPE_STRING,
                                    NACO_PARAM_SAVECUBE);

    bug_if (0);
    bug_if (contrib == NULL);

    /* Write the FITS file */
    skip_if (irplib_dfs_save_image(set, parlist, set, combined,
                                   CPL_BPP_IEEE_FLOAT, RECIPE_STRING,
                                   procatg, qclist, NULL,
                                   naco_pipe_id, RECIPE_STRING CPL_DFS_FITS));

    bug_if(cpl_propertylist_append_string(xtlist, "EXTNAME",
                                          "Contribution Map"));
    skip_if (cpl_image_save(contrib, RECIPE_STRING CPL_DFS_FITS,
                            CPL_BPP_16_SIGNED, xtlist, CPL_IO_EXTEND));

    if (is_cube) {

        bug_if(cpl_propertylist_update_string(xtlist, "EXTNAME",
                                              "Plane Properties"));
        skip_if (cpl_table_save(cubetable, NULL, xtlist,
                                RECIPE_STRING CPL_DFS_FITS, CPL_IO_EXTEND));
    }

    if (save_cube) {
        bug_if(cpl_propertylist_update_string(xtlist, "EXTNAME",
                                              "Corrected object images"));
        skip_if (cpl_imagelist_save(objimages, RECIPE_STRING CPL_DFS_FITS,
                                    CPL_BPP_IEEE_FLOAT, xtlist, CPL_IO_EXTEND));
    }

#ifdef NACO_SAVE_PAF
    skip_if (cpl_dfs_save_paf("NACO", RECIPE_STRING, paflist,
                             RECIPE_STRING CPL_DFS_PAF));
#else
    bug_if(paflist == NULL);
#endif

    end_skip;

    cpl_propertylist_delete(xtlist);

    return cpl_error_get_code();

}

/*----------------------------------------------------------------------------*/
/**
  @brief    Load the object images from a framelist
  @param    iscube      Valid, non-zero entry iff the image is the sum of a cube
  @param    self        The framelist
  @param    parlist     The recipe parameters
  @return   The loaded list of images or NULL on error.
  @see      cpl_image_load()
  @note The returned cpl_imagelist must be deallocated using
        cpl_imagelist_delete()

*/
/*----------------------------------------------------------------------------*/
static 
cpl_error_code naco_img_jitter_load_objimages(cpl_imagelist * objlist,
                                              cpl_array * iscube,
                                              const irplib_framelist * self,
                                              const cpl_parameterlist * parlist)
{
    const int nframes = irplib_framelist_get_size(self);
    cpl_image * image = NULL;
    int i;

    bug_if(objlist == NULL);
    bug_if(self    == NULL);
    bug_if(iscube  == NULL);
    bug_if(parlist == NULL);

    for (i=0; i < nframes; i++, image = NULL) {
        const char * filename
            = cpl_frame_get_filename(irplib_framelist_get_const(self, i));
        const cpl_propertylist * plist
            = irplib_framelist_get_propertylist_const(self, i);
        const int naxis3 = cpl_propertylist_has(plist, "NAXIS3") ? 
            cpl_propertylist_get_int(plist, "NAXIS3") : 1;


        if (naxis3 > 2) { /* Real cube mode has at least two frames + sum */

            bug_if(cpl_array_set_int(iscube, i, 1));

            /* EPompei 2009-11-13:
               The last frame in the cube is the sum of all the previous ones.
               This is not DICB compliant and may change.
               See also the NACO user manual.
            */

            image = cpl_image_load(filename, CPL_TYPE_FLOAT, naxis3-1, EXT0);

            any_if("Could not load %d-cube-sum from frame %d/%d, file=%s",
                   naxis3-1, i+1, nframes, filename);

        } else {
            image = cpl_image_load(filename, CPL_TYPE_FLOAT, 0, EXT0);

            any_if("Could not load FITS-image from extension %d in frame "
                   "%d/%d, file=%s", EXT0, i+1, nframes, filename);

        }
        bug_if(cpl_imagelist_set(objlist, image, i));
    }

    end_skip;

    cpl_image_delete(image);

    return cpl_error_get_code();

}

/*----------------------------------------------------------------------------*/
/**
  @brief Determine accuracy of final sum frame in cube
  @param self   The imagelist to process
  @param sum    The final sum image
  @return  CPL_ERROR_NONE on success, otherwise the relevant CPL error code

*/
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_img_jitter_check_cube(const cpl_imagelist * self,
                                                 const cpl_image * sum)
{
    cpl_image * accu = cpl_imagelist_collapse_create(self);
    double err;

    cpl_image_subtract(accu, sum);

    err = cpl_image_get_absflux(accu);

    if (err > 0.0) {
        err /= cpl_image_get_size_x(sum) * cpl_image_get_size_y(sum);

        cpl_msg_info(cpl_func, "Average per-pixel error in final sum frame "
                     "in %d-cube: %g", (int)cpl_imagelist_get_size(self), err);
    }

    cpl_image_delete(accu);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief Reprocess the cube-based images
  @param self      The imagelist to reprocess
  @param cubetable Table of cube combination data
  @param qclist    List of QC parameters
  @param iscube    Cubes images are flagged with a valid entry
  @param obj       The object framelist
  @param parlist   The recipe parameters
  @return  CPL_ERROR_NONE on success, otherwise the relevant CPL error code

*/
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_img_jitter_do_cube(cpl_imagelist * self,
                                              cpl_table * cubetable,
                                              cpl_propertylist * qclist,
                                              const cpl_array * iscube,
                                              const irplib_framelist * obj,
                                              const cpl_parameterlist * parlist)
{

    const int       nframes = irplib_framelist_get_size(obj);
    cpl_imagelist * onelist = NULL;
    cpl_image     * sum = NULL;
    cpl_image     * onesky = NULL;
    const int       nskyplane = naco_parameterlist_get_int(parlist,
                                                           RECIPE_STRING,
                                                           NACO_PARAM_SKYPLANE);
    cpl_vector    * skymedians = cpl_vector_new(nframes);
    int             nsky = 0;
    int             i;

    bug_if(self == NULL);
    bug_if(cubetable == NULL);
    bug_if(qclist == NULL);
    bug_if(obj == NULL);
    bug_if(parlist   == NULL);
    bug_if(nframes != cpl_array_get_size(iscube));
    bug_if(nframes != cpl_imagelist_get_size(self));


    for (i=0; i < nframes; i++) {
        int is_invalid = 0;

        (void)cpl_array_get_int(iscube, i, &is_invalid);

        if (!is_invalid) {
            const char * filename
                = cpl_frame_get_filename(irplib_framelist_get_const(obj, i));
            const cpl_propertylist * plist
                = irplib_framelist_get_propertylist_const(obj, i);
            double skymedian;
            int naxis3;

            /* Estimate the sky from nearby cube(s) */
            cpl_image_delete(onesky);
            onesky = naco_img_jitter_find_sky_cube(i, nskyplane, iscube, obj);
            any_if("Could not estimate sky for frame %d/%d, file=%s",
                   i+1, nframes, filename);

            skymedian = cpl_image_get_median(onesky);
#ifdef NACO_IMG_JITTER_DEVELOPMENT
            if (onelist == NULL) {

                /* FIXME: Compute from all cube-sky frames ? */
                /* Get the background value */
                bug_if(cpl_propertylist_append_double(qclist, "ESO QC BACKGD",
                                                      skymedian));

                cpl_image_save(onesky, "Sky.fits", CPL_BPP_IEEE_FLOAT, NULL,
                               CPL_IO_CREATE);
            } else {
                cpl_image_save(onesky, "Sky.fits", CPL_BPP_IEEE_FLOAT, NULL,
                               CPL_IO_EXTEND);
            }
#endif
            cpl_imagelist_delete(onelist);
            onelist = cpl_imagelist_load(filename, CPL_TYPE_FLOAT, EXT0);

            any_if("Could not load cube from frame %d/%d, file=%s",
                   i+1, nframes, filename);

            naxis3 = cpl_imagelist_get_size(onelist);

            cpl_msg_info(cpl_func, "Processing %d-cube from frame %d/%d, "
                         "file=%s. Median of sky-estimate: %g",
                         naxis3-1, i+1, nframes, filename, skymedian);
            cpl_vector_set(skymedians, nsky++, skymedian);

            bug_if(naxis3 < 3);

            sum = cpl_imagelist_unset(onelist, naxis3-1);

            bug_if(naco_img_jitter_check_cube(onelist, sum));

            skip_if(cpl_imagelist_subtract_image(onelist, onesky));

            cpl_image_delete(sum);
            sum = naco_img_jitter_combine_cube(cubetable, onelist, plist,
                                               parlist, i);
            any_if("Could not combine %d images from frame %d/%d, file=%s",
                   naxis3-1, i+1, nframes, filename);

            bug_if(cpl_imagelist_set(self, sum, i));
            sum = NULL;
        }
    }

    if (nsky > 1) {
        double skystdev, skymean;

        bug_if(cpl_vector_set_size(skymedians, nsky));

        skymean = cpl_vector_get_mean(skymedians);
        skystdev = cpl_vector_get_stdev(skymedians);

        cpl_msg_info(cpl_func, "Mean and stdev of %d medians of sky estimates: "
                     "%g %g", nsky, skymean, skystdev);

        bug_if(cpl_propertylist_append_double(qclist, "ESO QC SKY STDEV",
                                              skystdev));
    }

    end_skip;

    cpl_imagelist_delete(onelist);
    cpl_image_delete(sum);
    cpl_image_delete(onesky);
    cpl_vector_delete(skymedians);

    return cpl_error_get_code();
}



/*----------------------------------------------------------------------------*/
/**
  @brief Combine the frames in a cube to a single image
  @param cubetable Table of cube combination data
  @param cube    The sky subtracted cube to shift and add
  @param plist   The propertylist of the cube
  @param parlist The recipe parameters
  @param idx     The frame number (0 for first)
  @return  The combined image pair, or NULL on error

*/
/*----------------------------------------------------------------------------*/
static
cpl_image * naco_img_jitter_combine_cube(cpl_table * cubetable,
                                         const cpl_imagelist * cube,
                                         const cpl_propertylist * plist,
                                         const cpl_parameterlist * parlist,
                                         int idx)
{

    cpl_errorstate  prestate = cpl_errorstate_get();
    cpl_image     * self = NULL;
    const int       ncube = cpl_imagelist_get_size(cube);
    cpl_image    ** combined = NULL;
    cpl_apertures * apert = NULL;
    cpl_bivector  * offs = cpl_bivector_new(ncube);
    cpl_vector    * offsx = cpl_bivector_get_x(offs);
    cpl_vector    * offsy = cpl_bivector_get_y(offs);
    cpl_vector    * sigmas = NULL;
    cpl_vector    * vstrehl = cpl_vector_new(ncube);
    double          psigmas[] = {5.0, 2.0, 1.0, 0.5}; /* not modified */
    const int       nsigmas = (int)(sizeof(psigmas)/sizeof(double));
    /* Use half of default value to support images windowed to 128 x 130 */
    /* FIXME: Adjust according to image size ? */
    const double    rstar  = 0.5 * IRPLIB_STREHL_STAR_RADIUS;
    const double    rbgint = 0.5 * IRPLIB_STREHL_BACKGROUND_R1;
    const double    rbgext = 0.5 * IRPLIB_STREHL_BACKGROUND_R2;
    const double    pixscale = naco_pfits_get_pixscale(plist);
    const double    lucky = naco_parameterlist_get_double(parlist,
                                                          RECIPE_STRING,
                                                          NACO_PARAM_LUCK_STR);
    const char    * filter = naco_pfits_get_filter(plist);
    double          lam  = DBL_MAX; /* Avoid uninit warning */
    double          dlam = DBL_MAX; /* Avoid uninit warning */
    cpl_size        isigma = 0;
    const int       ncubetable = cpl_table_get_nrow(cubetable);
    int             icubetable; /* 1st table row to write to */
    int i;


    skip_if(rbgext <= rbgint);
    skip_if(pixscale <= 0.0);

    irplib_check(naco_get_filter_infos(filter, &lam, &dlam),
                 "Frame has no info for filter %s", filter);

    cpl_msg_debug(cpl_func, "Frame %d has pixelscale [Arcsecond/pixel]=%g, "
                  "Central wavelength [micron]=%g, Filter bandwidth "
                  "[micron]=%g", 1+idx, pixscale, lam, dlam);
    cpl_msg_debug(cpl_func, "Frame %d assumes Rstar [pixel]=%g, Rint "
                  "[pixel]=%g, Rext [pixel]=%g", 1+idx, rstar/pixscale,
                  rbgint/pixscale, rbgext/pixscale);

    if (cpl_table_get_ncol(cubetable) == 0) {
        cpl_table_new_column(cubetable, "Frame", CPL_TYPE_INT);
        cpl_table_new_column(cubetable, "Plane", CPL_TYPE_INT);
        cpl_table_new_column(cubetable, "XCentroid", CPL_TYPE_DOUBLE);
        cpl_table_new_column(cubetable, "YCentroid", CPL_TYPE_DOUBLE);
        cpl_table_set_column_unit(cubetable, "XCentroid", "pixel");
        cpl_table_set_column_unit(cubetable, "YCentroid", "pixel");
        cpl_table_new_column(cubetable, "Strehl", CPL_TYPE_DOUBLE);
        cpl_table_new_column(cubetable, "Strehl_Error", CPL_TYPE_DOUBLE);
        cpl_table_new_column(cubetable, "Star_Background", CPL_TYPE_DOUBLE);
        cpl_table_new_column(cubetable, "Star_Peak", CPL_TYPE_DOUBLE);
        cpl_table_new_column(cubetable, "Star_Flux", CPL_TYPE_DOUBLE);
        cpl_table_new_column(cubetable, "PSF_Peak_per_Flux", CPL_TYPE_DOUBLE);
        cpl_table_set_column_unit(cubetable, "PSF_Peak_per_Flux", "unitless");
        cpl_table_new_column(cubetable, "Background_Noise", CPL_TYPE_DOUBLE);
        cpl_table_set_size(cubetable, ncube);
        icubetable = 0;
    } else {
        cpl_table_set_size(cubetable, ncubetable + ncube);
        icubetable = ncubetable;
    }

    /* Create the vector for the detection thresholds */
    sigmas = cpl_vector_wrap(nsigmas, psigmas);
    for (i = 0; i < ncube; i++) {
        const cpl_image * one = cpl_imagelist_get_const(cube, i);
        double            cent_x, cent_y;
        double            strehl, strehl_err, star_bg,star_peak, star_flux;
        double            psf_peak, psf_flux, bg_noise;
        int               iflux;


        cpl_apertures_delete(apert);
        apert = cpl_apertures_extract(one, sigmas, &isigma);

        any_if("No object found in image %d/%d in frame %d", i+1, ncube, 1+idx);

        bug_if(irplib_apertures_find_max_flux(apert, &iflux, 1));

        cent_x = cpl_apertures_get_centroid_x(apert, iflux);
        cent_y = cpl_apertures_get_centroid_y(apert, iflux);
        cpl_vector_set(offsx, i, cent_x);
        cpl_vector_set(offsy, i, cent_y);

        cpl_msg_debug(cpl_func, "Detected %d/%d sigma=%g-aperture(s) (x,y)"
                      "=(%g,%g) (%d/%d) in %d/%d-image in frame %d", iflux,
                      (int)cpl_apertures_get_size(apert), psigmas[isigma],
                      cent_x, cent_y, 1+(int)isigma, nsigmas, 1+i, ncube,
                      1+idx);

        cpl_table_set_int(cubetable, "Frame", i+icubetable, 1+idx);
        cpl_table_set_int(cubetable, "Plane", i+icubetable, 1+i);
        cpl_table_set_double(cubetable, "XCentroid", i+icubetable, cent_x);
        cpl_table_set_double(cubetable, "YCentroid", i+icubetable, cent_y);


        if (naco_strehl_compute(one, parlist, RECIPE_STRING, lam, dlam,
                                cent_x, cent_y, pixscale,
                                  &strehl, &strehl_err,
                                  &star_bg, &star_peak, &star_flux,
                                  &psf_peak, &psf_flux,
                                  &bg_noise)) {
            cpl_msg_info(cpl_func, "Could not compute strehl for "
                         "image %d in frame %d", 1+i, 1+idx);
            cpl_errorstate_dump(prestate, CPL_FALSE,
                                irplib_errorstate_dump_debug);
            cpl_errorstate_set(prestate);

            cpl_table_set_invalid(cubetable, "Strehl", i+icubetable);
            cpl_table_set_invalid(cubetable, "Strehl_Error", i+icubetable);
            cpl_table_set_invalid(cubetable, "Star_Background", i+icubetable);
            cpl_table_set_invalid(cubetable, "Star_Peak", i+icubetable);
            cpl_table_set_invalid(cubetable, "Star_Flux", i+icubetable);
            cpl_table_set_invalid(cubetable, "PSF_Peak_per_Flux", i+icubetable);
            cpl_table_set_invalid(cubetable, "Background_Noise", i+icubetable);
            cpl_vector_set(vstrehl, i, 0.0);

        } else {
            cpl_vector_set(vstrehl, i, strehl);
            cpl_table_set_double(cubetable, "Strehl", i+icubetable, strehl);
            cpl_table_set_double(cubetable, "Strehl_Error", i+icubetable,
                                 strehl_err);
            cpl_table_set_double(cubetable, "Star_Background", i+icubetable,
                                 star_bg);
            cpl_table_set_double(cubetable, "Star_Peak", i+icubetable,
                                 star_peak);
            cpl_table_set_double(cubetable, "Star_Flux", i+icubetable,
                                 star_flux);
            cpl_table_set_double(cubetable, "PSF_Peak_per_Flux", i+icubetable,
                                 psf_peak/psf_flux);
            cpl_table_set_double(cubetable, "Background_Noise", i+icubetable,
                                 bg_noise);
        }
    }

    self = naco_img_jitter_saa_lucky(cube, vstrehl, offs, lucky);

    end_skip;

    if (combined != NULL) {
        cpl_image_delete(combined[0]);
        cpl_free(combined);
    }

     cpl_bivector_delete(offs);
    (void)cpl_vector_unwrap(sigmas);
    cpl_apertures_delete(apert);
    cpl_vector_delete(vstrehl);

    cpl_ensure(self != NULL, cpl_error_get_code(), NULL);

    return self;

}


/*----------------------------------------------------------------------------*/
/**
  @brief Estimate the sky in a given cube from nearby cube(s)
  @param isky      Estimate the sky of this cube (0 for first)
  @param nskyplane Number of planes to use from nearby cube(s), e.g. 25
  @param iscube    Cubes are flagged with a valid entry
  @param obj       The object framelist
  @return  CPL_ERROR_NONE on success, otherwise the relevant CPL error code

*/
/*----------------------------------------------------------------------------*/
static cpl_image * naco_img_jitter_find_sky_cube(int isky, int nskyplane,
                                                 const cpl_array * iscube,
                                                 const irplib_framelist * obj)
{

    cpl_image     * self = NULL;
    const int       nframes = irplib_framelist_get_size(obj);
    cpl_imagelist * belowcube = NULL;
    cpl_imagelist * abovecube = NULL;
    cpl_imagelist * skycube   = NULL;
    cpl_imagelist * mycube    = NULL;
    cpl_image     * mysky     = NULL;
    cpl_mask      * fillbpm   = NULL;
    int             is_invalid = 0;
    int             ibelow, iabove;
    int             i;

    bug_if(isky <  0);
    bug_if(isky >= nframes);
    bug_if(nframes != cpl_array_get_size(iscube));
    if (nskyplane > 0) skip_if_lt(nskyplane, 3, "sky planes for median");


    (void)cpl_array_get_int(iscube, isky, &is_invalid);

    bug_if(is_invalid); /* isky must be a cube */

    /* Find 1st cube below isky */
    for (i = isky - 1; i >= 0; i++) {

        (void)cpl_array_get_int(iscube, i, &is_invalid);

        if (!is_invalid) break;
    }

    ibelow = i; /* -1 means no cube below */

    /* Find 1st cube above isky */
    for (i = isky + 1; i < nframes; i++) {

        (void)cpl_array_get_int(iscube, i, &is_invalid);

        if (!is_invalid) break;
    }

    iabove = i; /* nframes means no cube above */

    cpl_msg_info(cpl_func, "Estimating sky for cube %d/%d via cubes %d and %d",
                 1+isky, nframes, 1+ibelow, 1+iabove);


    if (ibelow >= 0) {
        const char * filename
            = cpl_frame_get_filename(irplib_framelist_get_const(obj, ibelow));

        if (nskyplane > 0) {
            /* Load the last nskyplane frames */
            const cpl_propertylist * plist
                = irplib_framelist_get_propertylist_const(obj, ibelow);
            /* Ignore the last sum plane */
            const int istop  = irplib_pfits_get_int(plist, "NAXIS3") - 2;
            const int istart = NACO_MAX(0, istop - nskyplane + 1);

            skip_if_lt(istop - istart, 2, "sky planes for median");

            belowcube = cpl_imagelist_new();

            for (i = istart; i <= istop; i++) {
                self = cpl_image_load(filename, CPL_TYPE_FLOAT, i, EXT0);

                any_if("Could not load plane %d from frame %d/%d, file=%s",
                       1+i, 1+ibelow, nframes, filename);

                bug_if(cpl_imagelist_set(belowcube, self, i - istart));
            }

        } else {
            int naxis3;

            belowcube = cpl_imagelist_load(filename, CPL_TYPE_FLOAT, EXT0);

            any_if("Could not load cube from frame %d/%d, file=%s",
                   1+ibelow, nframes, filename);

            naxis3 = cpl_imagelist_get_size(belowcube);

            bug_if(naxis3 < 3);

            cpl_image_delete(cpl_imagelist_unset(belowcube, naxis3-1));

        }

    }

    if (iabove < nframes) {
        const char * filename
            = cpl_frame_get_filename(irplib_framelist_get_const(obj, iabove));

        if (nskyplane > 0) {
            /* Load the first nskyplane frames */
            const cpl_propertylist * plist
                = irplib_framelist_get_propertylist_const(obj, iabove);
            const int istart = 0;
            /* Ignore the last sum plane */
            const int istop  = NACO_MIN(nskyplane-1, irplib_pfits_get_int
                                        (plist, "NAXIS3") - 2);

            skip_if_lt(istop - istart, 2, "sky planes for median");

            abovecube = cpl_imagelist_new();

            for (i = istart; i <= istop; i++) {
                self = cpl_image_load(filename, CPL_TYPE_FLOAT, i, EXT0);

                any_if("Could not load plane %d from frame %d/%d, file=%s",
                       1+i, 1+iabove, nframes, filename);

                bug_if(cpl_imagelist_set(abovecube, self, i - istart));
            }

        } else {

            int naxis3;

            abovecube = cpl_imagelist_load(filename, CPL_TYPE_FLOAT, EXT0);

            any_if("Could not load cube from frame %d/%d, file=%s",
                   1+iabove, nframes, filename);

            naxis3 = cpl_imagelist_get_size(abovecube);

            bug_if(naxis3 < 3);

            cpl_image_delete(cpl_imagelist_unset(abovecube, naxis3-1));
        }

    }

    error_if(belowcube == NULL && abovecube == NULL, CPL_ERROR_DATA_NOT_FOUND,
             "No cube(s) available for sky estimation among %d object frames",
             nframes);

    if (belowcube == NULL) {
        skycube = abovecube;
    } else if (abovecube == NULL) {
        skycube = belowcube;
    } else {
        /* Wrap around the images in the two cubes */

        const int nbelow = cpl_imagelist_get_size(belowcube);
        const int nabove = cpl_imagelist_get_size(abovecube);
        int       nwrap = 0;

        skycube = cpl_imagelist_new();

        for (i = 0; i < nbelow; i++) {
            skip_if(cpl_imagelist_set(skycube, cpl_imagelist_get(belowcube, i),
                                      nwrap++));
        }
        for (i = 0; i < nabove; i++) {
            skip_if(cpl_imagelist_set(skycube, cpl_imagelist_get(abovecube, i),
                                      nwrap++));
        }
        skip_if(cpl_imagelist_get_size(skycube) != nwrap);
        skip_if(nbelow + nabove != nwrap);
    }

    self = cpl_imagelist_collapse_median_create(skycube);

#ifndef NACO_IMG_JITTER_KEEP_SKY_OBJECTS
    if (belowcube == NULL || abovecube == NULL) {
        /* Try to replace object-pixels with sky-pixels from the object image */
        const cpl_mask * fill2bpm;
        const cpl_mask * selfbpm = NULL;
        const double lo_skysigma = 0.2;
        const double hi_skysigma = 5.0;

        skip_if(naco_img_jitter_reject_objects(self, lo_skysigma, hi_skysigma));

        selfbpm = cpl_image_get_bpm_const(self);

        if (selfbpm != NULL) {
            const cpl_mask * mybpm;

            /* Substitute the objects pixels with sky pixels from isky */

            const char * filename
                = cpl_frame_get_filename(irplib_framelist_get_const(obj, isky));
            int naxis3;

            mycube = cpl_imagelist_load(filename, CPL_TYPE_FLOAT, EXT0);

            any_if("Could not load cube from frame %d/%d, file=%s",
                   1+isky, nframes, filename);

            naxis3 = cpl_imagelist_get_size(mycube);

            bug_if(naxis3 < 3);

            cpl_image_delete(cpl_imagelist_unset(mycube, naxis3-1));

            mysky = cpl_imagelist_collapse_median_create(mycube);

            skip_if(naco_img_jitter_reject_objects(mysky, lo_skysigma,
                                                   hi_skysigma));

            /* When a pixel is bad in self and good in mysky: Set to mysky */
            /* Other pixels in self are unchanged */

            mybpm = cpl_image_get_bpm_const(mysky);

            if (mybpm == NULL) {
                /* Fill all bad pixels in self with values from mysky */
                fill2bpm = selfbpm;
            } else {
                /* Fill those bad pixels in self with values from mysky
                   when those pixels are good */
                fillbpm = cpl_mask_duplicate(mybpm);
                bug_if(cpl_mask_not(fillbpm));
                bug_if(cpl_mask_and(fillbpm, selfbpm));

                fill2bpm = fillbpm;

            }
            cpl_msg_info(cpl_func, "Filling %d object-pixels in sky image "
                         "with %d sky-pixels from object image",
                         (int)cpl_mask_count(selfbpm),
                         (int)cpl_mask_count(fill2bpm));
            if (fill2bpm != selfbpm) {
                /* These rejected pixels will be filled */
                bug_if(cpl_image_reject_from_mask(self, fill2bpm));
            }

            if (fillbpm == NULL) {
                fillbpm = cpl_mask_duplicate(fill2bpm);
            } else if (fillbpm != fill2bpm) {
                bug_if(cpl_mask_copy(fillbpm, fill2bpm, 1, 1));
            }
            
            bug_if(cpl_image_fill_rejected(self, 0.0)); /* Use addition to fill */
            bug_if(cpl_image_accept_all(self)); /* fill2bpm may be invalid now */

            bug_if(cpl_mask_not(fillbpm));
            bug_if(cpl_image_reject_from_mask(mysky, fillbpm));
            bug_if(cpl_image_fill_rejected(mysky, 0.0)); /* Use addition to fill */
            bug_if(cpl_image_accept_all(mysky));
            bug_if(cpl_image_add(self, mysky));
        }
    }
#endif

    end_skip;

    if (cpl_error_get_code()) {
        cpl_image_delete(self);
        self = NULL;
    }


    if (skycube != belowcube && skycube != abovecube) {
        int nwrap = cpl_imagelist_get_size(skycube);

        /* Unwrap the wrapped images */
        for (;nwrap > 0;) {
            (void)cpl_imagelist_unset(skycube, --nwrap);
        }

        cpl_imagelist_delete(skycube);
    }

    cpl_mask_delete(fillbpm);
    cpl_image_delete(mysky);
    cpl_imagelist_delete(mycube);
    cpl_imagelist_delete(belowcube);
    cpl_imagelist_delete(abovecube);

    return self;
}


/*----------------------------------------------------------------------------*/
/**
  @brief Wrap an imagelist around the no-cube images
  @param self   Empty list to be filled
  @param iscube Cubes images are flagged with a valid entry
  @param other  The mix of cube and non-cube images
  @return  CPL_ERROR_NONE on success, otherwise the relevant CPL error code
  @note self must be emptied with (void)cpl_imagelist_unset() before it is
  deallocated.

*/
/*----------------------------------------------------------------------------*/
static
cpl_error_code naco_img_jitter_imagelist_wrap_nocube(cpl_imagelist * self,
                                                     const cpl_array * iscube,
                                                     cpl_imagelist * other)
{

    const int ncube = cpl_imagelist_get_size(other);
    int       nwrap = cpl_imagelist_get_size(self);
    int       i;

    bug_if(self   == NULL);
    bug_if(iscube == NULL);
    bug_if(other  == NULL);
    bug_if(nwrap != 0);
    bug_if(cpl_array_get_size(iscube) != ncube);

    for (i = 0; i < ncube; i++) {
        int is_invalid;

        (void)cpl_array_get_int(iscube, i, &is_invalid);

        if (is_invalid) {
            cpl_imagelist_set(self, cpl_imagelist_get(other, i), nwrap);
            nwrap++;
        }
    }

    bug_if(cpl_imagelist_get_size(self) != nwrap);

    end_skip;

    return cpl_error_get_code();
}


#ifndef NACO_IMG_JITTER_KEEP_SKY_OBJECTS

/*----------------------------------------------------------------------------*/
/**
  @brief Reject the pixels in objects above lo_sigma with parts above hi_sigma
  @param self     Image to be processed
  @param lo_sigma Low sigma-detection threshold (1/5 is a good value)
  @param hi_sigma High sigma-detection threshold (5 is a good value)
  @return  CPL_ERROR_NONE on success, otherwise the relevant CPL error code
  @note hi_sigma = 2 leads to false positives
  @see cpl_apertures_extract_sigma()

*/
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_img_jitter_reject_objects(cpl_image * self,
                                                     double lo_sigma, 
                                                     double hi_sigma)
{
    double       median;
    double       med_dist = DBL_MAX;
    double       hi_threshold;
    cpl_mask   * hi_objects = NULL;
    cpl_mask   * lo_objects = NULL;
    cpl_mask   * rejects = NULL;
    cpl_image  * hi_label = NULL;
    cpl_image  * lo_label = NULL;
    cpl_apertures * hi_apert = NULL;
    cpl_mask * kernel = NULL;


    bug_if(self == NULL);
    bug_if(lo_sigma <= 0.0);
    bug_if(hi_sigma < lo_sigma);

    /* Compute the threshold */
    median = cpl_image_get_median_dev(self, &med_dist);
    hi_threshold = median + hi_sigma * med_dist;

    /* Binarise the image with the high sigma threshold */
    hi_objects = cpl_mask_threshold_image_create(self, hi_threshold, DBL_MAX);
    bug_if(hi_objects == NULL);

    /* Apply a morphological opening to remove the single pixel detections */
    /* Copied from cpl_apertures_extract_sigma() */
    kernel = cpl_mask_new(3, 3);
    bug_if(cpl_mask_not(kernel));
    bug_if(cpl_mask_filter(hi_objects, hi_objects, kernel, CPL_FILTER_OPENING,
                           CPL_BORDER_ZERO));

    if (!cpl_mask_is_empty(hi_objects)) {
        /* Any low-sigma aperture overlapping a high-sigma
           aperture is assumed to be (all of) an object */

        const double lo_threshold = median + lo_sigma * med_dist;
        cpl_size hi_ilabel, hi_nlabel, lo_nlabel;

        /* Binarise the image with the low sigma threshold */
        lo_objects = cpl_mask_threshold_image_create(self, lo_threshold, DBL_MAX);
        bug_if(lo_objects == NULL);
        bug_if(cpl_mask_filter(lo_objects, lo_objects, kernel,
                               CPL_FILTER_OPENING, CPL_BORDER_ZERO));

        hi_label = cpl_image_labelise_mask_create(hi_objects, &hi_nlabel);
        lo_label = cpl_image_labelise_mask_create(lo_objects, &lo_nlabel);

        hi_apert = cpl_apertures_new_from_image(self, hi_label);
        bug_if(hi_apert == NULL);

        for (hi_ilabel = 1; hi_ilabel <= hi_nlabel; hi_ilabel++) {
            /* Get one pixel from the high-sigma aperture */
            const int pos_x = cpl_apertures_get_top_x(hi_apert, hi_ilabel);
            const int pos_y = cpl_apertures_get_top(hi_apert, hi_ilabel);
            /* The corresponding low-sigma aperture */
            int is_rejected;
            const int lo_ilabel = (int)cpl_image_get(lo_label, pos_x, pos_y,
                                                     &is_rejected);

            /* The mask of pixels with the corresponding low-sigma aperture */
            cpl_mask_delete(rejects);
            rejects = cpl_mask_threshold_image_create(lo_label,
                                                      (double)lo_ilabel - 0.5,
                                                      (double)lo_ilabel + 0.5);

            /* Add to the rejection mask */
            cpl_mask_or(hi_objects, rejects);

        }

        cpl_msg_info(cpl_func, "Found %d object(s) of %d pixel(s) "
                     "in sky image using sigmas %g and %g", (int)hi_nlabel,
                     (int)cpl_mask_count(hi_objects), lo_sigma, hi_sigma);
        bug_if(cpl_image_reject_from_mask(self, hi_objects));

    }

    end_skip;

    cpl_apertures_delete(hi_apert);
    cpl_image_delete(hi_label);
    cpl_image_delete(lo_label);
    cpl_mask_delete(kernel);
    cpl_mask_delete(hi_objects);
    cpl_mask_delete(lo_objects);
    cpl_mask_delete(rejects);

    return cpl_error_get_code();
}
#endif



/*----------------------------------------------------------------------------*/
/**
  @brief Lucky saa
  @param cube      The imagelist to process
  @param strehl    Corresponding Strehl ratios
  @param offs      Corresponding object offsets
  @param fraction  Top fraction of strehl ratios to accept
  @return The combined image pair or NULL on error

*/
/*----------------------------------------------------------------------------*/
static cpl_image * naco_img_jitter_saa_lucky(const cpl_imagelist * cube,
                                             const cpl_vector    * strehl,
                                             const cpl_bivector  * offs,
                                             double fraction)
{

    cpl_image * self = NULL;
    const int ncube = cpl_imagelist_get_size(cube);
    const int mcube = NACO_MAX(NACO_MIN(ncube, (int)(0.5 + fraction * ncube)),
                               1);
    cpl_imagelist * lcube   = NULL;
    const cpl_imagelist * ucube;
    cpl_vector    * lstrehl = NULL;
    /* Always need to duplicate due to pesky offset convention in
       cpl_geom_img_offset_saa() */
    cpl_bivector  * loffs   = cpl_bivector_duplicate(offs);
    cpl_vector    * loffsx  = cpl_bivector_get_x(loffs);
    cpl_vector    * loffsy  = cpl_bivector_get_y(loffs);
    cpl_table     * tsort = NULL;
    cpl_propertylist * psort = NULL;
    int i;


    bug_if(cpl_vector_get_size(strehl) != ncube);
    bug_if(cpl_bivector_get_size(offs) != ncube);
    bug_if(fraction <= 0.0);
    bug_if(fraction >  1.0);

    if (mcube < ncube) {
        double strehlmin; /* Smallest to be used */
        int  * pindex;

        lstrehl = cpl_vector_duplicate(strehl);

        tsort = cpl_table_new(ncube);
        psort = cpl_propertylist_new();

        /* Largest Strehl 1st */
        bug_if(cpl_propertylist_append_bool(psort, "LSTREHL", CPL_TRUE));

        bug_if(cpl_table_wrap_double(tsort, cpl_vector_get_data(lstrehl),
                                     "LSTREHL"));
        bug_if(cpl_table_wrap_double(tsort, cpl_vector_get_data(loffsx),
                                     "LOFFSX"));
        bug_if(cpl_table_wrap_double(tsort, cpl_vector_get_data(loffsy),
                                     "LOFFSY"));
        bug_if(cpl_table_new_column(tsort, "INDEX", CPL_TYPE_INT));

        /* The indices for the imagelist */
        pindex = cpl_table_get_data_int(tsort, "INDEX");
        for (i = 0; i < ncube; i++) {
            pindex[i] = i;
        }

        bug_if(cpl_table_sort(tsort, psort));
        /* Strehl, offsets and the (imagelist) indices have been sorted */

        lcube = cpl_imagelist_new();

        /* Use the index column to create a sorted list of wrapped images */
        for (i = 0; i < mcube; i++) {
            const int j = pindex[i];
            const cpl_image * image = cpl_imagelist_get_const(cube, j);

            cpl_imagelist_set(lcube, (cpl_image*)image, i);
        }
        /* tsort no longer accessed */

        /* loffs and the imagelist must both have length mcube */
        bug_if(cpl_vector_set_size(loffsx, mcube));
        bug_if(cpl_vector_set_size(loffsy, mcube));

        strehlmin = cpl_vector_get(lstrehl, mcube - 1);
        cpl_vector_delete(lstrehl);
        lstrehl = NULL;

        cpl_msg_info(cpl_func, "%g%% (%d/%d) lucky mode at Strehl=%g",
                     100.0*fraction, mcube, ncube, strehlmin);
    }
    ucube = lcube ? lcube : cube;

    self = naco_img_jitter_saa_center(ucube, loffs);
    any_if("Could not center and saa %d-cube", ncube);

    end_skip;

    if (lcube != NULL) {
        /* Unwrap the wrapped images */
        for (i = cpl_imagelist_get_size(lcube); i > 0;) {
            (void)cpl_imagelist_unset(lcube, --i);
        }

        cpl_imagelist_delete(lcube);
    }

    cpl_vector_delete(lstrehl);

    cpl_bivector_delete(loffs);

    if (tsort != NULL) {
        if (cpl_table_has_column(tsort, "LSTREHL"))
            (void)cpl_table_unwrap(tsort, "LSTREHL");
        if (cpl_table_has_column(tsort, "LOFFSX"))
            (void)cpl_table_unwrap(tsort, "LOFFSX");
        if (cpl_table_has_column(tsort, "LOFFSY"))
            (void)cpl_table_unwrap(tsort, "LOFFSY");
        cpl_table_delete(tsort);
    }
    cpl_propertylist_delete(psort);

    return self;
}


/*----------------------------------------------------------------------------*/
/**
  @brief Find the Strehl of a frame
  @param self      The images of the object frames
  @param objframes The object frames
  @return  CPL_ERROR_NONE on success, otherwise the relevant CPL error code

*/
/*----------------------------------------------------------------------------*/
static
cpl_error_code naco_img_jitter_find_strehl(const cpl_imagelist * self,
                                           const cpl_parameterlist * parlist,
                                           const irplib_framelist * objframes)
{

    const int       nobj = irplib_framelist_get_size(objframes);
    cpl_apertures * apert = NULL;
    cpl_vector    * sigmas = NULL;
    double          psigmas[] = {5.0, 2.0, 1.0, 0.5}; /* not modified */
    const int       nsigmas = (int)(sizeof(psigmas)/sizeof(double));
    cpl_size        isigma = 0;
    int             i;


    bug_if(cpl_imagelist_get_size(self) != nobj);

    /* Create the vector for the detection thresholds */
    sigmas = cpl_vector_wrap(nsigmas, psigmas);

    for (i = 0; i < nobj; i++) {
        const cpl_propertylist * plist
            = irplib_framelist_get_propertylist_const(objframes, i);
        const cpl_image * oimage = cpl_imagelist_get_const(self, i);

        const double      pixscale = naco_pfits_get_pixscale(plist);
        const char      * filter = naco_pfits_get_filter(plist);
        double            lam  = DBL_MAX; /* Avoid uninit warning */
        double            dlam = DBL_MAX; /* Avoid uninit warning */

        double            cent_x, cent_y;
        double            strehl = 0, strehl_err, star_bg,star_peak, star_flux;
        double            psf_peak, psf_flux, bg_noise;
        int               iflux;


        skip_if(pixscale <= 0.0);

        irplib_check(naco_get_filter_infos(filter, &lam, &dlam),
                     "Frame %d has no info for filter %s", 1+i, filter);

        cpl_apertures_delete(apert);
        apert = cpl_apertures_extract(oimage, sigmas, &isigma);

        any_if("No object found in combined image of frame %d", 1+i);

        bug_if(irplib_apertures_find_max_flux(apert, &iflux, 1));

        cent_x = cpl_apertures_get_centroid_x(apert, iflux);
        cent_y = cpl_apertures_get_centroid_y(apert, iflux);

        skip_if (naco_strehl_compute(oimage, parlist, RECIPE_STRING, lam, dlam,
                                     cent_x, cent_y, pixscale,
                                       &strehl, &strehl_err,
                                       &star_bg, &star_peak, &star_flux,
                                       &psf_peak, &psf_flux,
                                       &bg_noise));
        cpl_msg_info(cpl_func, "Image of frame %d/%d has strehl=%g at (x,y)"
                     "=(%g,%g)", 1+i, nobj, strehl, cent_x, cent_y);
    }

    end_skip;

    (void)cpl_vector_unwrap(sigmas);
    cpl_apertures_delete(apert);

    return cpl_error_get_code();
}



/*----------------------------------------------------------------------------*/
/**
  @brief Saa onto the centermost image
  @param cube   The imagelist to process
  @param offs   Corresponding object offsets - reordered and converted
  @return The combined image or NULL on error

*/
/*----------------------------------------------------------------------------*/
static cpl_image * naco_img_jitter_saa_center(const cpl_imagelist * cube,
                                              cpl_bivector * offs)
{

    const int       ncube  = cpl_imagelist_get_size(cube);
    cpl_image     * self   = NULL;
    cpl_image    ** combined = NULL;
    cpl_imagelist * ccube  = NULL;
    const cpl_imagelist * ucube;
    cpl_vector    * offsx  = cpl_bivector_get_x(offs);
    cpl_vector    * offsy  = cpl_bivector_get_y(offs);
    double        * doffsx = cpl_vector_get_data(offsx);
    double        * doffsy = cpl_vector_get_data(offsy);
    const double    med_x  = cpl_vector_get_median_const(offsx);
    const double    med_y  = cpl_vector_get_median_const(offsy);
    double          pos_x, pos_y;
    double          minsqdist = DBL_MAX;
    int             imin = -1;
    int             i;


    bug_if(cpl_bivector_get_size(offs) != ncube);

    /* Find image with object closest to object median location */
    for (i = 0; i < ncube; i++) {
        const double x = cpl_vector_get(offsx, i);
        const double y = cpl_vector_get(offsy, i);
        const double sqdist
            = (x - med_x) * (x - med_x) + (y - med_y) * (y - med_y);

        if (sqdist < minsqdist) {
            minsqdist = sqdist;
            imin = i;
        }
    }

    cpl_msg_info(cpl_func, "Plane %d/%d has minimal object distance %g "
                 "from median (x,y)=(%g,%g)", 1+imin, ncube,
                 sqrt(minsqdist), med_x, med_y);


    if (imin > 0) {
        /* Wrap ccube around the images - with image imin first */
        /* - the images from cube will _not_ be modified */
        const cpl_image * image = cpl_imagelist_get_const(cube, imin);

        ccube = cpl_imagelist_new();

        bug_if(cpl_imagelist_set(ccube, (cpl_image*)image, 0));

        for (i = 0; i < imin; i++) {
            image = cpl_imagelist_get_const(cube, i);
            bug_if(cpl_imagelist_set(ccube, (cpl_image*)image, 1+i));
        }
        for (i = 1+imin; i < ncube; i++) {
            image = cpl_imagelist_get_const(cube, i);
            bug_if(cpl_imagelist_set(ccube, (cpl_image*)image, i));
        }

        bug_if(cpl_imagelist_get_size(ccube) != ncube);

        /* Reorder offs accordingly */
        pos_x = cpl_vector_get(offsx, imin);
        pos_y = cpl_vector_get(offsy, imin);

        /* Move all offsets below imin 1 place up */
        (void)memmove(doffsx + 1, doffsx, (size_t)imin * sizeof(*doffsx));
        (void)memmove(doffsy + 1, doffsy, (size_t)imin * sizeof(*doffsy));

        /* Copy the imin offset to the 1st element */
        cpl_vector_set(offsx, 0, pos_x);
        cpl_vector_set(offsy, 0, pos_y);
    }
    ucube = ccube ? ccube : cube;

    /* Strange convention for the offsets :-(((((((((((((((( */
    cpl_vector_subtract_scalar(offsx, cpl_vector_get(offsx, 0));
    cpl_vector_subtract_scalar(offsy, cpl_vector_get(offsy, 0));
    cpl_vector_multiply_scalar(offsx, -1.0);
    cpl_vector_multiply_scalar(offsy, -1.0);

    combined = cpl_geom_img_offset_saa(ucube, offs, CPL_KERNEL_DEFAULT,
                                       0, 0, CPL_GEOM_FIRST, &pos_x, &pos_y);

    any_if("Could not shift and add %d-cube", ncube);

    cpl_msg_info(cpl_func, "Shift-and-added %d-cube, 1st pos=(%g,%g)",
                 ncube, pos_x, pos_y);

    self = combined[0];
    cpl_image_delete(combined[1]);
    combined[0] = NULL;
    combined[1] = NULL;

    end_skip;

    if (combined != NULL) {
        cpl_image_delete(combined[0]);
        cpl_image_delete(combined[1]);
        cpl_free(combined);
    }

    if (ccube != NULL) {
        /* Unwrap the wrapped no-cube images */
        for (i = cpl_imagelist_get_size(ccube); i > 0;) {
            (void)cpl_imagelist_unset(ccube, --i);
        }

        cpl_imagelist_delete(ccube);
    }

    return self;
}
