/* $Id: visir_inputs.c,v 1.206 2012/02/02 10:30:05 jtaylor Exp $
 *
 * This file is part of the VISIR 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: jtaylor $
 * $Date: 2012/02/02 10:30:05 $
 * $Revision: 1.206 $
 * $Name: visir-3_5_1 $
 */

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

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

#include <cpl.h>

/* TEMPORARY SUPPORT OF CPL 5.x */
#ifndef CPL_SIZE_FORMAT
#define CPL_SIZE_FORMAT "d"
#define cpl_size int
#endif
/* END TEMPORARY SUPPORT OF CPL 5.x */

#include "visir_utils.h"
#include "visir_pfits.h"
#include "visir_parameter.h"
#include "visir_pfits.h"
#include "visir_spc_distortion.h"
#include "visir_inputs.h"
#include "irplib_framelist.h"
#include "irplib_wcs.h"

#include <string.h>
#include <math.h>
#include <float.h>
#include <assert.h>

/*-----------------------------------------------------------------------------
                                   Define
 -----------------------------------------------------------------------------*/

#ifndef VISIR_DESTRIPE_DETECT
#define VISIR_DESTRIPE_DETECT           (3.5 * 1.64)
#endif

#ifndef VISIR_DESTRIPE_DETECT_THRESHOLD
#define VISIR_DESTRIPE_DETECT_THRESHOLD 1.3
#endif

#define VISIR_SECS_PER_DAY (24 * 3600)

typedef enum {
    VISIR_CUBE1,
    VISIR_CUBE2
} visir_cube_type;


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

#include "visir_destripe.h"

static cpl_image * visir_load_average(const char *,
                                      const cpl_propertylist *);
static cpl_imagelist * visir_load_intermint(const irplib_framelist *, int);
static cpl_error_code visir_imagelist_unpack_interm(cpl_imagelist *);
static cpl_error_code visir_rem_glitch(cpl_image *);
static cpl_error_code visir_rem_bad_images(cpl_imagelist *); 
static cpl_error_code visir_offset_hcycle(cpl_image *);

static cpl_image ** visir_img_collapse_beam_four(cpl_propertylist       *,
                                                 const cpl_image        *,
                                                 const cpl_image        *,
                                                 double, double,
                                                 const cpl_propertylist *);

static cpl_image ** visir_img_collapse_beam_three(cpl_propertylist       *,
                                                  const cpl_image        *,
                                                  const cpl_image        *,
                                                  double, double,
                                                  const cpl_propertylist *);


static cpl_error_code visir_img_find_beam_three(cpl_propertylist       *,
                                                const cpl_image        *,
                                                const cpl_image        *,
                                                double, double,
                                                double                 *,
                                                double                 *);

static cpl_error_code visir_img_find_beam_four(cpl_propertylist       *,
                                               const cpl_image        *,
                                               const cpl_image        *,
                                               double, double,
                                               double                 *,
                                               double                 *);

static cpl_error_code visir_get_type(const irplib_framelist *, int,
                                     visir_cube_type *, int *, int *);


/*----------------------------------------------------------------------------*/
/**
 * @defgroup visir_inputs   Functions to handle VISIR input data
 */
/*----------------------------------------------------------------------------*/

/**@{*/


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    Load and split a chunk of planes from a CUBE2 file
  @param    alist       The list to be filled with A-frames
  @param    blist       The list to be filled with B-frames
  @param    packed      The chunk of planes to be split
  @param    prevd       The difference image of the previous chunk.
                        NULL for first chunk in a file.
  @return   CPL_ERROR_NONE, or the relevant CPL_ERROR code.
  @see visir_load_intermint()

 */
/*----------------------------------------------------------------------------*/

static
cpl_error_code visir_load_cube2_split_(cpl_imagelist * alist, cpl_imagelist * blist,
                                       cpl_imagelist * packed, cpl_image * prevd)
{

    int naxis3 = cpl_imagelist_get_size(packed);
    int prevd_insert_pos = cpl_imagelist_get_size(blist);

    bug_if(alist     == NULL);
    bug_if(blist     == NULL);


    if (prevd)
        cpl_imagelist_set(blist, prevd, prevd_insert_pos);

    /* Split the cube */
    for (int i = 0; i < naxis3/2; i++) {
        cpl_image * aimage = cpl_imagelist_unset(packed, 0); /* Ai */
        cpl_image * dimage = cpl_imagelist_unset(packed, 0); /* Ai - Bi mean */

        cpl_imagelist_set(alist, aimage, cpl_imagelist_get_size(alist));
        cpl_imagelist_set(blist, dimage, cpl_imagelist_get_size(blist));

    }

    skip_if_lt(0, cpl_imagelist_get_size(packed), "Too many packed frames");

    /* After this call the ith image in the list is Ai - Bi */
    bug_if(visir_imagelist_unpack_interm(blist));

    /* remove extra image again */
    if (prevd)
        cpl_imagelist_unset(blist, prevd_insert_pos);

    for (int i = 0; i < naxis3/2; i++) {
        const cpl_image * aimage = cpl_imagelist_get_const(alist, i);
        const cpl_image * dimage = cpl_imagelist_get_const(blist, i);
        /* Compute Bi from Ai - (Ai - Bi). */
        cpl_image       * bimage = cpl_image_subtract_create(aimage, dimage);

        /* Deallocate the difference image and set Bi in its place */
        bug_if(cpl_imagelist_set(blist, bimage, i));

    }

    skip_if_lt(0, cpl_imagelist_get_size(packed), "Too many packed frames");

    end_skip;

    return cpl_error_get_code();

}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    Load a range of planes from a file
  @param    filename    Name of the file to load from.
  @param    im_type     Type of the image
  @param    pstart      Plane range start point. Counting from 0.
  @param    pend        Plane range end point. Counting from 0.
  @return   1 newly allocated imagelist
  @see visir_load_intermint()

 */
/*----------------------------------------------------------------------------*/
static cpl_imagelist * load_range(const char * filename,
                                  cpl_type     im_type,
                                  int          pstart,
                                  int          pend)
{
    int        selfsize = 0;
    cpl_imagelist * self = cpl_imagelist_new();

    for (int iplane = pstart; iplane < pend; iplane++) {
        cpl_image * image = cpl_image_load(filename, im_type, iplane, 0);

        if (image == NULL) break;

        if (cpl_imagelist_set(self, image, selfsize)) {
            cpl_image_delete(image);
            break;
        }

        selfsize++;
    }
    return self;
}


/*----------------------------------------------------------------------------*/
/**
  @brief    Load and split the data in a CUBE2 file
  @param    alist       The list to be filled with A-frames
  @param    blist       The list to be filled with B-frames
  @param    rawframes   The rawframes
  @param    pos         The position of the frame
  @param    planestart  Number of the first frame to load. Counting from 0
  @param    planeend    Number of the last frame to load + 1. Counting from 0
  @return   CPL_ERROR_NONE, or the relevant CPL_ERROR code.
  @see visir_load_intermint()

  The input VISIR file must have the format CUBE2 (DET FRAM TYPE = CUBE2) :
    - A1 (HCYCLE frame nb 1)
    - A1-B1
    - ...
    - An (HCYCLE frame nb n)
    - (A1-B1+...+An-Bn)/n
    - INT

  This function gets the chopping image set A1-B1, ..., (A1-B1+...+An-Bn)/n
  (also named INTERM-INT) out of it.
 */
/*----------------------------------------------------------------------------*/
cpl_error_code visir_load_cube2_split(cpl_imagelist * alist, cpl_imagelist * blist,
                                      const irplib_framelist * rawframes, int pos,
                                      const int planestart, const int planeend)
{

    cpl_imagelist      * packed = NULL;
    const char         * file
        = cpl_frame_get_filename(irplib_framelist_get_const(rawframes, pos));
    int                  naxis3, nchop;
    visir_cube_type      cube_type;
    int pend;
    cpl_image * prevd = NULL;

    skip_if(0);

    (void)visir_get_type(rawframes, pos, &cube_type, &naxis3, &nchop);

    /* both ranges must be even except for the last chunk where it must
     * be uneven for the average image at the end of the CUBE2 file */
    error_if(planestart % 2 == 1, CPL_ERROR_ILLEGAL_INPUT,
             "Plane start %d wrong. It must be even.", planestart);
    error_if(planeend < naxis3 && planeend % 2 == 1, CPL_ERROR_ILLEGAL_INPUT,
             "Plane end %d wrong. It must be even if not larger naxis3=%d",
             planeend, naxis3);

    any_if("Cannot split non-CUBE2 frame %d/%d in %s", 1+pos,
           irplib_framelist_get_size(rawframes), file);
    bug_if(alist     == NULL);
    bug_if(blist     == NULL);

    error_if(cube_type != VISIR_CUBE2, CPL_ERROR_INCOMPATIBLE_INPUT, "Cannot "
             "split non-CUBE2 frame %d/%d w. NAXIS3=%d, NCYCLES=%d in %s",
             1+pos, irplib_framelist_get_size(rawframes), naxis3, nchop, file);

    if (planeend >= naxis3 || planeend < 0)
        pend = naxis3 - 1; /* don't load sum */
    else
        pend = planeend;

    if (planestart != 0) {
        packed = load_range(file, CPL_TYPE_UNSPECIFIED, planestart - 1, pend);
        prevd = cpl_imagelist_unset(packed, 0);
    }
    else
        packed = load_range(file, CPL_TYPE_UNSPECIFIED, planestart, pend);

    skip_if(visir_load_cube2_split_(alist, blist, packed, prevd));

    end_skip;

    cpl_image_delete(prevd);
    cpl_imagelist_delete(packed);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    Load and split the imagelist in a BURST file
  @param    alist       The list to be filled with on-frames
  @param    blist       The list to be filled with off-frames
  @param    packed      The imagelist to split
  @param    ichopchange First plane the chopper moves from on to off
  @param    ihalfcycle  Number of planes in an half exposure
  @param    trimlow     Number of planes to delete before chopper movement plane
  @param    trimhigh    Number of planes to delete after chopper movement plane
                        if set to -1 it does not delete the movement plane
  @return   CPL_ERROR_NONE, or the relevant CPL_ERROR code.

  Reads in BURST data and splits it into on and off exposures lists.
  Skips planes +- trim{low,high} where the chopper moves based.
 */
/*----------------------------------------------------------------------------*/
static
cpl_error_code visir_load_burst_(cpl_imagelist * alist, cpl_imagelist * blist,
                                 cpl_imagelist * packed,
                                 const int ichopchange, const int ihalfcycle,
                                 const int trimlow, int const trimhigh)
{
    cpl_boolean bon = CPL_TRUE;
    const int offset = 0;
    const int pend = cpl_imagelist_get_size(packed);
    int lorej = ihalfcycle - trimlow;
    int hirej = trimhigh + 1; /* +1 to reject the actual changing frame too */

    /* get frame where chopper moved from on to off in the _previous_
     * half exposure */
    int chpmv =  ichopchange - ihalfcycle * 2;

    cpl_ensure_code(trimhigh >= -1, CPL_ERROR_ILLEGAL_INPUT);
    cpl_ensure_code(trimlow >= 0, CPL_ERROR_ILLEGAL_INPUT);
    cpl_ensure_code(ichopchange < ihalfcycle * 2, CPL_ERROR_ILLEGAL_INPUT);
    cpl_ensure_code(alist != NULL, CPL_ERROR_NULL_INPUT);
    cpl_ensure_code(blist != NULL, CPL_ERROR_NULL_INPUT);

    skip_if(0);

    /* Split the cube and skip the planes where the chopper
     * changes +- a safety margin
     * always starts from on -> off movement even if negative */
    for (int i = chpmv; i < pend; i++) {
        if ((i + ihalfcycle * 2) % ihalfcycle == ichopchange % ihalfcycle) {
            bon = !bon;
            hirej = trimhigh + 1;
            lorej = ihalfcycle - trimlow;
        }
        if (hirej <= 0 && lorej > 0 && i >= 0) {
            if (bon) {
                cpl_image * image= cpl_imagelist_unset(packed, offset);
                cpl_imagelist_set(alist, image, cpl_imagelist_get_size(alist));
            }
            else {
                cpl_image * image= cpl_imagelist_unset(packed, offset);
                cpl_imagelist_set(blist, image, cpl_imagelist_get_size(blist));
            }
        }
        else if (i >= 0)
            cpl_image_delete(cpl_imagelist_unset(packed, offset));

        hirej--;
        lorej--;
    }

    cpl_msg_info(cpl_func, "On: %d, Off %d, Skipped %d",
                 (int)cpl_imagelist_get_size(alist),
                 (int)cpl_imagelist_get_size(blist),
                 (int)(pend - cpl_imagelist_get_size(alist)
                            - cpl_imagelist_get_size(blist)));

    skip_if_lt(0, cpl_imagelist_get_size(packed), "Too many packed frames");

    end_skip;

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    get plane where chopper moved from on to off accounting for offset
  @param    chopchange  global first plane where chopper moved from on to off
  @param    offset      plane offset 
  @param    ihalfcycle  number of frames in a half exposure
  @return   first plane of on -> off movement after offset
 */
/*----------------------------------------------------------------------------*/
static int get_to_off_plane(int chopchange, const int offset,
                            const int ihalfcycle)
{
    /* if offset in second hcycle beginning on/off state is flipped */
    if (offset -
        (offset / (ihalfcycle * 2)) * ihalfcycle * 2 > chopchange)
        chopchange += ihalfcycle * 2 - (offset % (ihalfcycle * 2));
    else
        chopchange -= (offset % ihalfcycle);
    return chopchange;
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    Load and split BURST frame
  @param    alist       The list to be filled with A-frames
  @param    blist       The list to be filled with B-frames
  @param    frame       The frame to split
  @param    plist       Property list of the frame
  @param    ichopchange First plane the chopper moves from on to off
  @param    ihalfcycle  Number of planes in an half exposure
  @param    planestart  Plane number offset
  @param    planeend    Plane number end point
  @param    trimlow     Number of planes to delete before chopper movement plane
  @param    trimhigh    Number of planes to delete after chopper movement plane
  @return   CPL_ERROR_NONE, or the relevant CPL_ERROR code.

  Reads in BURST data and splits it into on and off exposures lists.
  Skips planes +- trim{low,high} where the chopper moves.
 */
/*----------------------------------------------------------------------------*/
cpl_error_code visir_load_burst(cpl_imagelist * alist, cpl_imagelist * blist,
                                const cpl_frame * frame, const cpl_propertylist * plist,
                                const int ichopchange, const int ihalfcycle,
                                const int planestart, const int planeend,
                                const int trimlow, const int trimhigh)
{
    const char * file = cpl_frame_get_filename(frame);
    const int    naxis3 = visir_pfits_get_naxis3(plist);
    const int    pend = planeend <= 0 || planeend > naxis3 ? naxis3 : planeend;
    cpl_imagelist * packed;
    int to_off = get_to_off_plane(ichopchange, planestart, ihalfcycle);

    cpl_msg_info(cpl_func, "Loading planes %d to %d, to off %d",
                 planestart, pend, planestart + to_off);

    packed = load_range(file, CPL_TYPE_UNSPECIFIED, planestart, pend);

    skip_if(packed == NULL);

    skip_if(visir_load_burst_(alist, blist, packed, to_off, ihalfcycle,
                              trimlow, trimhigh));

    end_skip;

    cpl_imagelist_delete(packed);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief  Collapse the 3/4 beams of a combined image
  @param  qclist     Append QC parameters here
  @param  self       The combined image to collapse
  @param  parlist    Parameterlist w. maximum allowed eccentricity
  @param  recipename The recipe name in the parameterlist
  @param  mode       VISIR_CHOPNOD_PERPENDICULAR, _PARALLEL or _AUTO
  @param  plist      Propertylist of the 1st raw image
  @return A pointer to the newly allocated image array or NULL on error
  @see      visir_img_recombine()

  The returned images array contains 2 images:
  - the combined image
  - the contribution map
       
  If not null, the returned image array arr must be deallocated like this:
  cpl_image_delete(arr[0]);
  cpl_image_delete(arr[1]);
  cpl_free(arr);

 */
/*----------------------------------------------------------------------------*/
cpl_image ** visir_img_collapse_beam(cpl_propertylist        * qclist,
                                     const cpl_image         * self,
                                     const cpl_parameterlist * parlist,
                                     const char              * recipename,
                                     visir_chopnod_mode        mode,
                                     const cpl_propertylist  * plist)
{

    cpl_image ** combined = NULL;
    /* Need to invert the negative beams */
    cpl_image  * inverse = cpl_image_multiply_scalar_create(self, -1.0);

    const double eccmax   = visir_parameterlist_get_double(parlist, recipename,
                                                           VISIR_PARAM_ECCMAX);

    /* Get the chopping throw in pixels */
    const char * sscale       = visir_pfits_get_pixscale(plist);
    const double pscale       = sscale ? atof(sscale) : 0.0;
    const double pthrow       = pscale > 0.0
        ? visir_pfits_get_chop_throw(plist) / pscale : 0.0;


    skip_if(self    == NULL);
    skip_if(parlist == NULL);
    skip_if(qclist  == NULL);
    skip_if(plist   == NULL);

    if (mode == VISIR_CHOPNOD_PERPENDICULAR) {
        /* 4 sources */
        combined = visir_img_collapse_beam_four(qclist, self, inverse, eccmax,
                                                pthrow, plist);
    } else if (mode == VISIR_CHOPNOD_PARALLEL) {
        /* 3 sources */
        combined = visir_img_collapse_beam_three(qclist, self, inverse, eccmax,
                                                 pthrow, plist);
    } else if (mode == VISIR_CHOPNOD_AUTO) {
        cpl_errorstate cleanstate = cpl_errorstate_get();

        const char   * sdir = visir_pfits_get_chopnod_dir(plist);

        if (sdir != NULL && !strcmp(sdir, "PERPENDICULAR")) {
            /* 4 sources */
            combined = visir_img_collapse_beam_four(qclist, self, inverse,
                                                  eccmax, pthrow, plist);
        } else if (sdir != NULL && !strcmp(sdir, "PARALLEL")) {
            /* 3 sources */
            combined = visir_img_collapse_beam_three(qclist, self, inverse,
                                                     eccmax, pthrow, plist);
        } else {
            if (sdir == NULL) {
                visir_error_reset("Could not get FITS key");
            } else {
                cpl_msg_warning(cpl_func, "Unknown chopping direction: %s",
                                sdir);
            }
            cpl_msg_warning(cpl_func, "Proceeding as if FITS card "
                            VISIR_PFITS_STRING_CHOPNOD_DIR " had value: %s",
                            "PERPENDICULAR");
            combined = visir_img_collapse_beam_four(qclist, self, inverse,
                                                    eccmax, pthrow, plist);
            if (combined == NULL) {
                visir_error_reset("Proceeding as if FITS card "
                                  VISIR_PFITS_STRING_CHOPNOD_DIR
                                  " had value: %s", "PARALLEL");
                combined = visir_img_collapse_beam_three(qclist, self, inverse,
                                                         eccmax, pthrow, plist);
            }
        }
    } else {
        bug_if(1);
    }

    skip_if(combined == NULL);

    bug_if (cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM THROW",
                                           pthrow));
    bug_if (cpl_propertylist_set_comment(qclist, "ESO QC ONEBEAM THROW",
                                         "The throw in pixels (TEL CHOP THROW "
                                         "divided by INS PFOV)"));

    bug_if (cpl_propertylist_set_comment(qclist, "ESO QC ONEBEAM XPOS",
                                         "The X pixel position (centroid) "
                                         "of the one-beam object"));

    bug_if (cpl_propertylist_set_comment(qclist, "ESO QC ONEBEAM YPOS",
                                         "The Y pixel position (centroid) "
                                         "of the one-beam object"));
    bug_if (cpl_propertylist_set_comment(qclist, "ESO QC ONEBEAM ECCENTRICITY",
                                         "Eccentricity: 0 for perfect, throw-"
                                         "sized square/line"));

    end_skip;

    cpl_image_delete(inverse);

    return combined;
}


/*----------------------------------------------------------------------------*/
/**
  @brief    The VISIR input data re-combination is performed here
  @param    recipename  The name of the calling recipe
  @param    parlist     hte recipe parameter list
  @param    rawframes   the frames list
  @param    badpix      the optionally user provided bad pixels map
  @param    flat        the optionally user provided flat field
  @param    nodding_p   Optional, pre-allocated array for nodding positions
  @param    do_spc_fix  True iff spectro distortion correction is to be done
  @param    wlen        Central wavelength (ignored unless do_spc_fix is true)
  @param    resol       Spectral resolution (ignored unless do_spc_fix is true)
  @return   a pointer to the newly allocated nodded images set or NULL.

  The input list of files has to contain an even number if files: 2*n.

  Let's suppose n=2. We have here 4 input files. 
  
  Each file is a 'INTERM' type of file produced by IRACE. If A and B are the 
  two images produced by the 2 chopping positions, an 'INTERM' file contains 
  (example with 3 chopping cycles):

  A1-B1
  ((A1-B1) + (A2-B2)) / 2
  ((A1-B1) + (A2-B2) + (A3-B3)) / 3

  The first step of the algorithm is to convert each of these 4 files in:

  A1-B1
  A2-B2
  A3-B3

  The glitches are removed from each image of each image set (if requested).

  The image sets are purged from their bad images (if requested).
 
  Each input file/cube (f) is then averaged (avg(f)) and normalised by a factor
  defined like this:
  factor(f) = nod_pos(f) * DIT * nimages * 2 
  where nod_pos(f) is 1 is the object is observed in the center of the image
  and -1 if the sky is observed in the center (object is on the side).

  Each pair of files (f1, f2) is then reduce to one nodded image:
  nodded = (avg(f1)/factor(f1) + avg(f2)/factor(f2))/2
 
  Each nodded image is then flatfielded and bad pixels corrected if requested.

  If no bad pixel map is provided, and if the auto_bpm flag is set, the first 
  file hcycle image is used to determine a map of bad pixels: the pixels 
  whose value is bigger than VISIR_HCYCLE_BPM_THRESHOLD are flagged as bad.

  Each nodded image (there should be 2 in this example) shall represent 
  a bright star in the center with 2 shadows. The shadow comes from the 
  chopping (A-B), and it is duplicated because of the nodding (2 files used 
  to create nodded).

  The nodded images are returned in an image set.
  
  Two modes are allowed : the staring mode where all nod_pos=1 (no nodding) 
  and the nodding mode where in each pair, there must be one nod_pos=1 and 
  one nod_pos=-1.

  The NODPOS keyword is used to determine if the object (val=A, nod_pos=1) 
  or the sky (val=B, nod_pos=-1) is in the center.
  If this keyword is missing in the header, a user can provide an ascii file 
  containing 0 (for A positions) or 1 (for B positions) in each line (as 
  many lines as input files) :
  # Example with 4 input files
  1 0
  2 1
  3 1
  4 0

  If the passed nodding_p is not NULL, it must be a pointer to an allocated 
  integers array containing one element per input frame.
  Something like
  nodding_p = cpl_malloc(nb_input_frames * sizeof(int));
  must be done by the caller function.
  nodding_p is filled with 1 if the file correspond to an object observation, 
  and with -1 for the sky.
  
  The returned image set must be deallocated with cpl_imagelist_delete().
 */
/*----------------------------------------------------------------------------*/
cpl_imagelist * visir_inputs_combine(const char * recipename,
                                     const cpl_parameterlist * parlist,
                                     const irplib_framelist * rawframes,
                                     const char      *   badpix,
                                     const char      *   flat,
                                     int             *   nodding_p,
                                     cpl_boolean         do_spc_fix,
                                     double              wlen,
                                     visir_spc_resol resol)
{
    const char       * fnodpos;
    int                nfiles;
    cpl_imagelist    * in = NULL;
    cpl_image        * collapsed = NULL;
    cpl_image        * prev = NULL;
    cpl_vector       * nods_vec = NULL;
    double           * nods_data;
    int              * nod_pos = NULL;
    cpl_image       ** images = NULL;
    cpl_imagelist    * nodded = NULL;
    int                nnod;
    cpl_image        * flat_image = NULL;
    cpl_image        * bpm_im_int = NULL;
    cpl_mask         * bpm_im_bin = NULL;
    cpl_imagelist    * hcycle = NULL;
    cpl_boolean        no_rem;
    cpl_boolean        is_nodding = CPL_FALSE;
    int                i, j;
    cpl_boolean        auto_bpm, rem_glitch, rem_bad;
    int                ndestripe;
    int                naxis1, naxis2;
    cpl_boolean        morpho_destripe;
    double             tstart, tstop;
    const cpl_propertylist * plist1;
#ifdef _OPENMP
    cpl_errorstate     cleanstate = cpl_errorstate_get();
#endif
    cpl_error_code     didfail = CPL_ERROR_NONE;


    skip_if (0);
    skip_if(recipename == NULL);
    skip_if(parlist    == NULL);
    skip_if(rawframes  == NULL);
    
    /* Get the number of files */
    nfiles = irplib_framelist_get_size(rawframes);

    /* There should be an even number of files */
    if (nfiles % 2) {
        cpl_msg_warning(cpl_func, "Expecting even number of files, "
                        "ignoring the last of %d file(s)", nfiles);
        error_if (nfiles == 1, CPL_ERROR_DATA_NOT_FOUND,
                  "At least two files are required");
        nfiles--;
    }

    nnod = nfiles/2;

    skip_if (nnod <= 0);

    skip_if(irplib_framelist_contains(rawframes, VISIR_PFITS_STRING_FRAME_TYPE,
                                       CPL_TYPE_STRING, CPL_FALSE, 0.0));
    skip_if(irplib_framelist_contains(rawframes, VISIR_PFITS_DOUBLE_DIT,
                                       CPL_TYPE_DOUBLE, CPL_FALSE, 0.0));
    /* Can only combine images of the same size */
    skip_if (irplib_framelist_contains(rawframes, "NAXIS1",
                                       CPL_TYPE_INT, CPL_TRUE, 0.0));
    skip_if (irplib_framelist_contains(rawframes, "NAXIS2",
                                       CPL_TYPE_INT, CPL_TRUE, 0.0));
    skip_if(irplib_framelist_contains(rawframes, VISIR_PFITS_INT_NAXIS3,
                                       CPL_TYPE_INT, CPL_FALSE, 0.0));
    skip_if(irplib_framelist_contains(rawframes, VISIR_PFITS_INT_CHOP_NCYCLES,
                                       CPL_TYPE_INT, CPL_FALSE, 0.0));

    plist1 = irplib_framelist_get_propertylist_const(rawframes, 0);
    naxis1 = irplib_pfits_get_int(plist1, "NAXIS1");
    naxis2 = irplib_pfits_get_int(plist1, "NAXIS2");
    skip_if(0);

    /* Retrieve input parameters */
    fnodpos = visir_parameterlist_get_string(parlist, recipename,
                                             VISIR_PARAM_NODPOS);
    skip_if (0);

    auto_bpm = visir_parameterlist_get_bool(parlist, recipename,
                                            VISIR_PARAM_AUTOBPM);
    skip_if (0);

    rem_glitch = visir_parameterlist_get_bool(parlist, recipename,
                                              VISIR_PARAM_GLITCH);
    skip_if (0);

    rem_bad = visir_parameterlist_get_bool(parlist, recipename,
                                           VISIR_PARAM_PURGE);
    skip_if (0);

    ndestripe = visir_parameterlist_get_int(parlist, recipename,
                                            VISIR_PARAM_STRIPITE);
    bug_if (0);

    morpho_destripe = ndestripe <= 0 ? CPL_FALSE :
        visir_parameterlist_get_bool(parlist, recipename,
                                     VISIR_PARAM_STRIPMOR);
    bug_if (0);

    no_rem = !rem_glitch && !rem_bad;

    /* Each file corresponds to a nodding position (object=1 or sky=-1) */
    /* Return nod_pos array if requested */
    nod_pos = nodding_p ? nodding_p : cpl_malloc(nfiles * sizeof(int));
    j = 0;
    if (fnodpos != NULL && strlen(fnodpos) > 0 && strcmp(fnodpos, ".") != 0) {
        /* Get the nodding positions from the user-provided ascii file */
        nods_vec = cpl_vector_read(fnodpos);
        skip_if (cpl_vector_get_size(nods_vec) != nfiles);
        nods_data = cpl_vector_get_data(nods_vec);
        skip_if (0);
        for (i=0 ; i<nfiles ; i++) {
            if ((int)nods_data[i] == 0) {
                nod_pos[i] = 1;
                j++;
            } else if ((int)nods_data[i] == 1) {
                nod_pos[i] = -1;
                is_nodding = CPL_TRUE;
            } else {
                error_if(1, CPL_ERROR_BAD_FILE_FORMAT,
                         "Wrong values in line %d in %s", i+1, fnodpos);
            }
        }
    } else {
        skip_if (irplib_framelist_contains(rawframes, VISIR_PFITS_STRING_NODPOS,
                                            CPL_TYPE_STRING, CPL_FALSE, 0.0));
    }

    if (no_rem) cpl_msg_info(cpl_func, "No glitch removal and no purge of bad "
                             "frames requested: Using fast I/O method");

    /* Initialize the Bad Pixel Map */
    if (badpix != NULL) {
        /* The bpm is provided by the user */
        cpl_msg_info(cpl_func, "Loading bad pixel map from %s", badpix);
        /* Load the bad pixels image */
        bpm_im_int = cpl_image_load(badpix, CPL_TYPE_INT, 0, 0);
        skip_if (0);

        /* Convert the map from integer to binary */
        bpm_im_bin = cpl_mask_threshold_image_create(bpm_im_int, -0.5, 0.5);
        cpl_image_delete(bpm_im_int);
        bpm_im_int = NULL;
        skip_if (cpl_mask_not(bpm_im_bin));
    } else if (auto_bpm) {
        /* Initialize the Bad Pixel Map using the hcycle */

        /* i == 0 */
        hcycle = visir_load_imagelist(rawframes, 0, CPL_FALSE);
        skip_if(0);

        bpm_im_bin =
            cpl_mask_threshold_image_create(cpl_imagelist_get(hcycle,0),
                                            VISIR_HCYCLE_BPM_THRESHOLD,
                                            DBL_MAX);
        cpl_imagelist_delete(hcycle);
        hcycle = NULL;
        skip_if(0);
    }

    /* Initialize the flat field image */
    if (flat != NULL) {
        cpl_msg_info(cpl_func, "Divide the nodded images by the flatfield");
        /* Load the flat image */
        flat_image = cpl_image_load(flat, CPL_TYPE_FLOAT, 0, 0);
        any_if ("Cannot load the flat field %s", flat ? flat : "<NULL>");
    }
 
    /* Get nodding position (if needed) and DIT from the header */
    nodded = cpl_imagelist_new();

    tstart = cpl_test_get_walltime();

    /* Fill list with empty images of proper size and type */

    for (i=0; i < nfiles/2 ; i++) {
        cpl_image * empty = cpl_image_new(naxis1, naxis2, CPL_TYPE_FLOAT);

        /* i'th image can only be inserted when i-1'th is there,
           which prevents parallelism */

        bug_if (cpl_imagelist_set(nodded, empty, i));
    }

#ifdef _OPENMP
#pragma omp parallel for private(i) firstprivate(prev, collapsed)       \
                         schedule(static, 2)
#endif
    for (i = 0; i < nfiles ; i++) {
        cpl_error_code errori = cpl_error_get_code();

        /* The total number of iterations must be pre-determined for the
           parallelism to work. In case of an error we can therefore not
           break, so instead we skip immediately to the next iteration.
           FIXME: This check on didfail does not guarantee that only one
                  iteration can cause an error to be dumped, but it is not
                  worse than checking on the thread-local state, errori. */
        if (didfail) continue;

        do {

        const char * file =
            cpl_frame_get_filename(irplib_framelist_get_const(rawframes, i));
        const cpl_propertylist * plist;

        double dit;
        double factor;


        plist = irplib_framelist_get_propertylist_const(rawframes, i);
        if (plist == NULL) {
            errori = cpl_error_set_where(cpl_func);
            break;
        }

        if (nods_vec == NULL) {
            const char * sval = visir_pfits_get_nodpos(plist);
            if (sval == NULL) {
                errori = cpl_error_set_message(cpl_func,
                                                CPL_ERROR_DATA_NOT_FOUND,
                                                "Cannot get nodding position "
                                                "for file %d/%d", i+1, nfiles);
                break;
            }
            if (!strcmp(sval, "A")) {
                nod_pos[i] = 1;
                j++;
            } else {
                nod_pos[i] = -1;
                is_nodding = CPL_TRUE;
            }
        }

        /* Print the file name with its nodding position */
        cpl_msg_info(cpl_func, "File %02d: %s (%c)", i+1, file,
                     nod_pos[i]==1 ? '+' : '-');

        /* With nodding each pair must have exactly one object observation */
        if (is_nodding && (i & 1) == 1 && nod_pos[i] == nod_pos[i-1]) {
            cpl_msg_error(cpl_func, "Nodding pair (%d,%d) does not comprise an "
                          "on-object (A) and an off-object (B) image: %s", i-1,
                          i, nod_pos[i] == 1 ? "A" : "B");
        }

        /* Compute the normalization factor from the Detector Integration Time */
        dit = visir_pfits_get_dit(plist);
        if (cpl_error_get_code()) {
            errori = cpl_error_set_where(cpl_func);
            break;
        }

        if (dit <= 0) {
            errori = cpl_error_set_message(cpl_func,
                                            CPL_ERROR_ILLEGAL_INPUT,
                                            "DIT in file %d/%d is too small: "
                                            "%g", i+1, nfiles, dit);
            break;
        }

        factor = dit * nod_pos[i] * 2.0;

        if (no_rem){
            collapsed = visir_load_average(file, plist);
        } else {
            in = visir_load_intermint(rawframes, i);
            if (in == NULL) {
                errori = cpl_error_set_message(cpl_func,
                                                CPL_ERROR_ILLEGAL_INPUT,
                                                "Could not load image set %d",
                                                i+1);
                break;
            }

            /* Convert the image lists from 'INTERM' to A-B' */
            if (visir_imagelist_unpack_interm(in)) {
                errori = cpl_error_set_message(cpl_func,
                                                cpl_error_get_code(),
                                                "Failure for file %d/%d",
                                                i+1, nfiles);
                break;
            }

            /* Remove the glitch in each A-B image in each input cube
               if requested */
            if (rem_glitch) {
                int jj;
                for (jj=0 ; jj < cpl_imagelist_get_size(in); jj++) {
                    if (visir_rem_glitch(cpl_imagelist_get(in, jj))) {
                        errori = cpl_error_set_message(cpl_func,
                                                        cpl_error_get_code(),
                                                        "Could not remove "
                                                        "glitch in image %d in "
                                                        "set %d", jj+1, i+1);
                        break;
                    }
                }
            }

            /* Remove the bad A-B images in each input file/cube if requested */
            if (rem_bad) {
                cpl_msg_info(cpl_func, "Remove the bad A-B input images");
                if (visir_rem_bad_images(in)) {
                    errori = cpl_error_set_message(cpl_func,
                                                    cpl_error_get_code(),
                                                    "Could not remove bad "
                                                    "images in list %d", i+1);
                    break;
                }
            }
            /* Average each cube */
            collapsed = cpl_imagelist_collapse_create(in);

            cpl_imagelist_delete(in);
            in = NULL;

        }

        if (cpl_error_get_code()) {
            errori = cpl_error_set_message(cpl_func, cpl_error_get_code(),
                                            "Failure for file %d/%d",
                                            i+1, nfiles);
            break;
        }
        /* Normalise to have ADU/s. */
        /* Also divide with 2 to achieve average of image pair */
        if (cpl_image_divide_scalar(collapsed, 2*factor)) {
            errori = cpl_error_set_message(cpl_func, cpl_error_get_code(),
                                            "Failure for file %d/%d",
                                            i+1, nfiles);
            break;
        }

        /* Each pair of input files gives a nodded image in nodded */
        if (i & 1) {
            if (cpl_image_add(prev, collapsed)) {
                errori = cpl_error_set_message(cpl_func, cpl_error_get_code(),
                                                "Failure for file %d/%d",
                                                i+1, nfiles);
                break;
            }
            cpl_image_delete(collapsed);
            collapsed = NULL;

            /* At this point prev is the image to be put into
               the list of nodded images */

            if (bpm_im_bin != NULL) {
                /* Apply the bad pixels cleaning */
                if (cpl_image_reject_from_mask(prev, bpm_im_bin)) {
                    errori = cpl_error_set_message(cpl_func,
                                                    cpl_error_get_code(),
                                                    "Failure for file %d/%d",
                                                    i+1, nfiles);
                    break;
                }
                if (cpl_detector_interpolate_rejected(prev)) {
                    errori = cpl_error_set_message(cpl_func,
                                                    cpl_error_get_code(),
                                                    "Failure for file %d/%d",
                                                    i+1, nfiles);
                    break;
                }
            }

            if (ndestripe > 0)
                if(visir_destripe_image(prev, ndestripe,
                                        VISIR_DESTRIPE_DETECT,
                                        VISIR_DESTRIPE_DETECT_THRESHOLD,
                                        morpho_destripe)) {
                    errori = cpl_error_set_message(cpl_func,
                                                    cpl_error_get_code(),
                                                    "Failure for file %d/%d",
                                                    i+1, nfiles);
                    break;
                }

            if (flat_image != NULL) {
                /* Apply the flatfield correction */
                if (cpl_image_divide(prev, flat_image)) {
                    errori = cpl_error_set_message(cpl_func,
                                                    cpl_error_get_code(),
                                                    "Failure for file %d/%d",
                                                    i+1, nfiles);
                    break;
                }
            }

            if (cpl_imagelist_set(nodded, prev, i/2)) {
                errori = cpl_error_set_message(cpl_func, cpl_error_get_code(),
                                               "Failure for file %d/%d",
                                               i+1, nfiles);
                break;
            }
            prev = NULL;
        } else {
            prev = collapsed;
            collapsed = NULL;
        }
        } while (0);

        if (errori) {
#ifdef _OPENMP
            /* Cannot access these errors after the join,
               so dump them now. :-(((((((((((((((((((( */
            cpl_errorstate_dump(cleanstate, CPL_FALSE, NULL);
            cpl_errorstate_set(cleanstate);

            /* Need to deallocate the thread-private instances in the loop */
            cpl_image_delete(prev); prev = NULL;
            cpl_image_delete(collapsed); collapsed = NULL;

#pragma omp critical(visir_inputs_combine)
#endif
            didfail = errori;
        }
    }

    error_if(didfail, didfail, "Failed to create %d nodded images from %d "
             "files", nnod, nfiles);

    tstop = cpl_test_get_walltime();
    cpl_msg_info(cpl_func, "Time to create %d nodded images [s]: %g", nnod,
                 tstop - tstart);

    cpl_vector_delete(nods_vec);
    nods_vec = NULL;

    cpl_image_delete(flat_image);
    flat_image = NULL;

    cpl_mask_delete(bpm_im_bin);
    bpm_im_bin = NULL;

    if (nod_pos != nodding_p) cpl_free(nod_pos);
    nod_pos = NULL;
    
    error_if(is_nodding && j != nnod, CPL_ERROR_INCOMPATIBLE_INPUT,
         "With nodding exactly half of the images "
             "must be on-object, not %d of %d", j, 2*nnod);

    if (do_spc_fix) {
        const double ksi   = visir_parameterlist_get_double(parlist, recipename,
                                                          VISIR_PARAM_SPECSKEW);
        const double eps   = visir_parameterlist_get_double(parlist, recipename,
                                                            VISIR_PARAM_VERTARC);
        const double delta = visir_parameterlist_get_double(parlist, recipename,
                                                            VISIR_PARAM_HORIARC);
        const double phi   = visir_parameterlist_get_double(parlist, recipename,
                                                          VISIR_PARAM_SLITSKEW);
        const int doplot   = visir_parameterlist_get_int(parlist, recipename,
                                                         VISIR_PARAM_PLOT);


        skip_if (0);

        images = cpl_malloc(nnod * sizeof(cpl_image*));

        for (j = 0; j < nnod; j++) images[j] = cpl_imagelist_get(nodded, j);

        skip_if (visir_spc_det_fix(images, nnod, CPL_TRUE, wlen, resol,
                                   phi, ksi, eps, delta, doplot));
    }

    end_skip;

    cpl_msg_set_time_off();

    cpl_free(images);
    cpl_imagelist_delete(in);

    if (nod_pos != nodding_p) cpl_free(nod_pos);
    cpl_vector_delete(nods_vec);
    cpl_image_delete(bpm_im_int);
    cpl_mask_delete(bpm_im_bin);
    cpl_image_delete(collapsed);
    cpl_image_delete(prev);
    if (cpl_error_get_code() && nodded != NULL) {
        cpl_imagelist_delete(nodded);
        nodded = NULL;
    }

    return nodded;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    The VISIR imaging combination using cross correlation
  @param    recipename   The name of the calling recipe
  @param    parlist      the recipe parameter list
  @param    rawframes    the list of raw frames
  @param    badpix       the optionally user provided bad pixels map
  @param    flat         the optionally user provided flat field
  @param    combine_mode The combine mode (first, union, intersect)
  @param    pdid_resize  Set to true if the combined image was resized
  @param    do_spc_fix   True iff spectro distortion correction is to be done
  @param    wlen         Central wavelength (ignored unless do_spc_fix is true)
  @param    resol        Spectral resolution (ignored unless do_spc_fix is true)
  @return   a pointer to the newly allocated image array or NULL on error
  @see      visir_inputs_combine()

  The input files are first recombined to create the nodded images using
  visir_inputs_combine().

  The created nodded images are then shifted and added to create the 
  final combined image. The offsets stored in the SEQ.CUMOFFSETX and Y 
  keywords of the frames whose nod_pos==1 are used as first estimate. 
  If the offsets keywords are missing in the header, a user can provide an 
  ascii file containing the x and y offsets of each input pair of file as here:
  # Example with 4 input files
  # file nb.    x offset    y offset 
       1               0           0  # correspond to file 1
       2             -10          10  # correspond to file 4
 
  A cross-correlation method is used to refine the offsets before the shift&add.
  The cross-correlation needs anchor points that typically are bright objects.
  These are automatically detected but can also be provided by the user through
  an ascii file containing as many lines as objects, and for each one the x 
  and y positions of the object in the image:
  # Example with 1 object
  # obj nb.    x pos    y pos
       1        125      135          # correspond to object 1

  The returned images array contains 2 images:
  - the combined image
  - the contribution map
       
  If not null, the returned image array arr must be deallocated like this:
  cpl_image_delete(arr[0]);
  cpl_image_delete(arr[1]);
  cpl_free(arr);
 */
/*----------------------------------------------------------------------------*/
cpl_image ** visir_img_recombine(const char * recipename,
                                 const cpl_parameterlist * parlist,
                                 const irplib_framelist  * rawframes,
                                 const char        * badpix,
                                 const char        * flat,
                                 cpl_geom_combine    combine_mode,
                                 cpl_boolean       * pdid_resize,
                                 cpl_boolean         do_spc_fix,
                                 double              wlen,
                                 visir_spc_resol     resol)
{
    int                     nfiles;
    int                 *   nod_pos = NULL;
    cpl_imagelist       *   nodded = NULL;
    int                     nnod;
    cpl_bivector        *   offsets_est = NULL;
    cpl_bivector        *   objs = NULL;
    cpl_image           **  combined = NULL;
    const cpl_vector    *   sigmas = NULL;
    double                  xoff, yoff;
    cpl_propertylist    *   qclist = cpl_propertylist_new();
    int                     i;


    bug_if (0);

    bug_if (recipename == NULL);
    bug_if (parlist    == NULL);
    bug_if (rawframes  == NULL);
    bug_if (pdid_resize  == NULL);

    /* Get the number of files */
    nfiles = irplib_framelist_get_size(rawframes);

    /* There should be an even number of files */
    if (nfiles % 2) {
        cpl_msg_warning(cpl_func, "Expecting even number of files, "
                        "ignoring the last of %d file(s)", nfiles);
        error_if (nfiles == 1, CPL_ERROR_DATA_NOT_FOUND,
                  "At least two files are required");
        nfiles--;
    }

    skip_if ( nfiles <= 0);

    /* Each file corresponds to a nodding position (object=1 or sky=-1) */
    nod_pos = cpl_malloc(nfiles * sizeof(int));

    /* Combine the input frames into the nodded images */
    cpl_msg_info(cpl_func, "Combining the input frames into the nodded images");
    nodded = visir_inputs_combine(recipename, parlist, rawframes, badpix, flat,
                                  nod_pos, do_spc_fix, wlen, resol);

    skip_if (nodded == NULL);
    nnod = cpl_imagelist_get_size(nodded);

    /* If only one nodded image, the work is finished */
    if (nnod == 1) {

        combined = cpl_malloc(2*sizeof(cpl_image*)); 
        combined[1] = NULL; /* In case the unset fails */

        combined[0] = cpl_imagelist_unset(nodded, 0);
        bug_if (combined[0] == NULL);

        combined[1] = cpl_image_new(cpl_image_get_size_x(combined[0]),
                                    cpl_image_get_size_y(combined[0]),
                                    CPL_TYPE_INT);
        bug_if (combined[1] == NULL);

        /* Set all pixel values to 1 */
        bug_if(cpl_image_threshold(combined[1], 1.0, 1.0, 1.0, 1.0));

        *pdid_resize = CPL_FALSE;

    } else {
        const double psigmas[] = {5, 2, 1, 0.5};
        const char * sval;
        const char * offsets;
        const char * objects;
        int sx, sy, mx, my;
        int rej_low, rej_high;
        cpl_boolean refine;


        refine = visir_parameterlist_get_bool(parlist, recipename,
                                              VISIR_PARAM_REFINE);
        skip_if (0);

        offsets = visir_parameterlist_get_string(parlist, recipename,
                                                 VISIR_PARAM_OFFSETS);
        skip_if (0);

        objects = visir_parameterlist_get_string(parlist, recipename,
                                                 VISIR_PARAM_OBJECTS);
        skip_if (0);

        sval = visir_parameterlist_get_string(parlist, recipename,
                                              VISIR_PARAM_XCORR);
        skip_if (0);

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


        sval = visir_parameterlist_get_string(parlist, recipename,
                                              VISIR_PARAM_REJECT);
        skip_if (0);

        if (sscanf(sval, "%d-%d", &rej_low, &rej_high) !=2 )
            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");
        if (offsets != NULL && strlen(offsets) && strcmp(offsets, ".") != 0) {
            /* A file has been provided on the command line */
            offsets_est = cpl_bivector_read(offsets);
            skip_if (offsets_est==NULL);

            error_if (cpl_bivector_get_size(offsets_est) != nnod,
                      CPL_ERROR_BAD_FILE_FORMAT, "The offsets file %s must "
                      "have %d entries, not %d", offsets, nnod,
                      (int)cpl_bivector_get_size(offsets_est));
        } else {
            double * offsets_est_x;
            double * offsets_est_y;
            double   xoff0 = 0.0;  /* Avoid (false) uninit warning */
            double   yoff0 = 0.0;  /* Avoid (false) uninit warning */
            
            /* Get the offsets from the header */
            offsets_est = cpl_bivector_new(nnod);
            offsets_est_x = cpl_bivector_get_x_data(offsets_est);
            offsets_est_y = cpl_bivector_get_y_data(offsets_est);

            skip_if (0);

            offsets_est_x[0] = 0.0;
            offsets_est_y[0] = 0.0;

            for (i=0; i < nnod ; i++) {
                const cpl_propertylist * plist;

                /* Skip every other, non-object frame */ 
                const int iframe = nod_pos[2*i] == 1 ? 2*i : 2*i+1;

                plist = irplib_framelist_get_propertylist_const(rawframes,
                                                                iframe);
                skip_if(plist == NULL);

                xoff = visir_pfits_get_cumoffsetx(plist);
                yoff = visir_pfits_get_cumoffsety(plist);

                skip_if (0);

                if (i == 0) {
                    xoff0 = xoff;
                    yoff0 = yoff;
                } else {
                    /* Subtract the first offset from all offsets */
                    offsets_est_x[i] = xoff0 - xoff;
                    offsets_est_y[i] = yoff0 - yoff;
                }
            }
        }
        cpl_free(nod_pos);
        nod_pos = NULL;
    
        /* Read the provided objects file if provided
           - if a file has been provided on the command line */
        if (objects != NULL && strlen(objects) > 0 && strcmp(offsets, ".") != 0) {
            objs = cpl_bivector_read(objects);
            any_if ("Could not read objects from %s", objects);
        }
   
        cpl_msg_info(cpl_func, "Recombining the list of nodded images using "
                     "mode: %d (I=%d:U=%d:F=%d), rej-lo=%d, rej-hi=%d",
                     combine_mode, CPL_GEOM_INTERSECT, CPL_GEOM_UNION,
                     CPL_GEOM_FIRST, rej_low, rej_high);

        if (cpl_msg_get_level() <= CPL_MSG_DEBUG) {
            cpl_msg_debug(cpl_func, "The offsets for the recombination:");
            cpl_bivector_dump(offsets_est, stdout);
        }

        sigmas = cpl_vector_wrap(4, (double*)psigmas); /* Not changed */
        combined = cpl_geom_img_offset_combine(nodded, offsets_est, refine,
                                               objs, sigmas, NULL, sx, sy,
                                               mx, my, rej_low, rej_high,
                                               combine_mode);
        any_if("Could not recombine the images");

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

    if (visir_parameterlist_get_int(parlist, recipename, VISIR_PARAM_PLOT) > 0)
        visir_image_plot("", "t 'The combined image'", "", combined[0]);

    end_skip;

    cpl_propertylist_delete(qclist);
    cpl_free(nod_pos);
    cpl_imagelist_delete(nodded);
    cpl_bivector_delete(offsets_est);
    cpl_bivector_delete(objs);
    cpl_vector_unwrap((cpl_vector*)sigmas);

    return combined;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Load the HCYCLE images from a VISIR file
  @param    rawframes The rawframes
  @param    pos       The frame to load
  @return   The newly allocated image set with the HCYCLE frames 

  The input file is a VISIR file can have the format CUBE1 
  (DET FRAM TYPE = CUBE1) :
    - chopping cube (INTERM-INT: A1-B1, ..., (A1-B1+...+An-Bn)/n)
    - first HCYCLE frame
    - INT image
    
  or the format CUBE2 (DET FRAM TYPE = CUBE2) :
    - A1 (HCYCLE frame nb 1)
    - A1-B1
    - ...
    - Ai (HCYCLE frame nb n)
    - (A1-B1+...+An-Bn)/n
    - INT
    
  This function gets the HCYCLE images 
 */
/*----------------------------------------------------------------------------*/
cpl_imagelist * visir_load_hcycle(const irplib_framelist * rawframes, int pos)
{
    return visir_load_imagelist(rawframes, pos, CPL_FALSE);
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Reject the hot pixels in an image
  @param    self    The image with hot pixels
  @param    bpmfile  Name of file with bad-pixel-map, or NULL
  @return   CPL_ERROR_NONE, or the relevant CPL_ERROR code.

  Iff a name of a bad pixel map FITS file is provided it is used for the
  cleaning. Otherwise auto-cleaning is done.

 */
/*----------------------------------------------------------------------------*/
cpl_error_code visir_image_reject_hot(cpl_image * self, const char * bpmfile)
{

    cpl_image  * im_bpm = NULL;
    cpl_mask   * bpm = NULL;
    const int upper = VISIR_HCYCLE_BPM_THRESHOLD;


    skip_if (0);

    skip_if (self == NULL);

    if (bpmfile == NULL) {
        bpm = cpl_mask_threshold_image_create(self, upper, DBL_MAX);
        skip_if (0);
    } else {

        /* The bpm is provided by the user */
        cpl_msg_info(cpl_func, "Clean user specified bad pixels");
        /* Load the bad pixel image */
        im_bpm = cpl_image_load(bpmfile, CPL_TYPE_INT, 0, 0);
        any_if ("Could not load the bad pixel map %s",
                bpmfile ? bpmfile : "<NULL>");
        /* Convert the map from integer to binary */
        bpm = cpl_mask_threshold_image_create(im_bpm, -0.5, 0.5);
        skip_if (0);
        cpl_image_delete(im_bpm);
        im_bpm = NULL;

        skip_if (cpl_mask_not(bpm));
    }

    skip_if (cpl_image_reject_from_mask(self, bpm));

    end_skip;

    cpl_image_delete(im_bpm);
    cpl_mask_delete(bpm);

    return cpl_error_get_code();

}


/*----------------------------------------------------------------------------*/
/**
  @brief    Load the last frame of the input files to an image list
  @param    rawframes     List of frames with identical number of planes
  @return   the imagelist or NULL on error

 */
/*----------------------------------------------------------------------------*/
cpl_imagelist * visir_imagelist_load_last(const irplib_framelist * rawframes)
{
    cpl_imagelist * self = NULL;
    int naxis3;

    /* Verify that NAXIS3 is the same in all files */
    skip_if(irplib_framelist_contains(rawframes, VISIR_PFITS_INT_NAXIS3,
                                      CPL_TYPE_INT, CPL_TRUE, 0.0));

    naxis3 = visir_pfits_get_naxis3(irplib_framelist_get_propertylist_const(
                                                                      rawframes,
                                                                      0));

    /* Load the image set */
    self = irplib_imagelist_load_framelist(rawframes, CPL_TYPE_FLOAT, naxis3-1,
                                           0);

    skip_if (self == NULL);

    end_skip;

    return self;

}


/*----------------------------------------------------------------------------*/
/**
  @brief    Load either a INTERM or Half-Cycle cube from a VISIR file
  @param    rawframes   The rawframes
  @param    pos         The position of the frame
  @param    is_interm True iff the the requested image type is INTERM
  @return   the newly allocated imagelist
  @see visir_load_intermint().

 */
/*----------------------------------------------------------------------------*/
cpl_imagelist * visir_load_imagelist(const irplib_framelist * rawframes,
                                     int pos, cpl_boolean is_interm)
{
    cpl_imagelist    * self = NULL;
    cpl_image        * image = NULL;
    const char       * file
        = cpl_frame_get_filename(irplib_framelist_get_const(rawframes, pos));
    int                naxis3, nchop;
    visir_cube_type    cube_type;
    const cpl_error_code code = visir_get_type(rawframes, pos, &cube_type,
                                               &naxis3, &nchop);
    int            plane_step;
    int            nsize;
    int            iplane;
    int            i;

    /* Check the error code */
    skip_if (code);
    bug_if (file == NULL);

    if (cube_type == VISIR_CUBE1) {
        /* All INTERM frames are in the first part of the cube */
        iplane = is_interm ? 0 : nchop;
        /* - followed by a single Half-cycle image */
        nsize  = is_interm ? nchop : 1;
        plane_step = 1;
    } else if (naxis3 == 1) {
        iplane = 0;
        nsize  = 1;
        plane_step = 1;
    } else {
        /* Each INTERM frame follows the Half-cycle frame */
        iplane = is_interm ? 1 : 0;
        nsize  = nchop;
        plane_step = 2;
    }

    self = cpl_imagelist_new();

    for (i=0 ; i < nsize; i++, iplane += plane_step) {

        image = cpl_image_load(file, CPL_TYPE_FLOAT, iplane, 0);
        skip_if (image == NULL);

        skip_if (!is_interm && visir_offset_hcycle(image));

        skip_if (cpl_imagelist_set(self, image, i));

        image = NULL;
    }

    skip_if (i < nsize);

    end_skip;

    if (cpl_error_get_code()) {
        if (file != NULL) cpl_msg_warning(cpl_func, "Could not load the %s "
                                          "frame(s) from: %s",
                                          is_interm ? "INTERM" : "Half-Cycle",
                                          file);
        cpl_image_delete(image);
        cpl_imagelist_delete(self);
        self = NULL;
    }

    return self;
}


/**@}*/

/*----------------------------------------------------------------------------*/
/**
  @brief    Load a chopping 'INTERM-INT' cube from a VISIR file
  @param    rawframes   The rawframes
  @param    pos         The position of the frame
  @return   the newly allocated image set A1-B1,..., (A1-B1+...+An-Bn)/n 

  The input file is a VISIR file can have the format CUBE1 
  (DET FRAM TYPE = CUBE1) :
    - chopping cube (INTERM-INT: A1-B1, ..., (A1-B1+...+An-Bn)/n)
    - first HCYCLE frame
    - INT image
    
  or the format CUBE2 (DET FRAM TYPE = CUBE2) :
    - A1 (HCYCLE frame nb 1)
    - A1-B1
    - ...
    - Ai (HCYCLE frame nb n)
    - (A1-B1+...+An-Bn)/n
    - INT
    
  This function gets the chopping image set A1-B1, ..., (A1-B1+...+An-Bn)/n 
  (also named INTERM-INT) out of it.
 */
/*----------------------------------------------------------------------------*/
static cpl_imagelist * visir_load_intermint(const irplib_framelist * rawframes,
                                            int pos)
{
    return visir_load_imagelist(rawframes, pos, CPL_TRUE);

}

/*----------------------------------------------------------------------------*/
/**
  @brief    Load the last INTERM image from a VISIR file
  @param    file      The filename
  @param    plist    propertylist of file
  @return   the newly allocated image
  @see visir_load_intermint().

 */
/*----------------------------------------------------------------------------*/
static cpl_image * visir_load_average(const char * file,
                                      const cpl_propertylist * plist)
{
    cpl_errorstate cleanstate = cpl_errorstate_get();
    cpl_image        * self = NULL;
    int                nchop, naxis3;


    skip_if (0);
    skip_if (file   == NULL);
    skip_if (plist == NULL);

    naxis3 = visir_pfits_get_naxis3(plist);
    nchop = visir_pfits_get_chop_ncycles(plist);

    skip_if (0);

    if (nchop == 0 && naxis3 == 1) {
        self = cpl_image_load(file, CPL_TYPE_FLOAT, 0, 0);
    } else {
        const char   * sval;
        /* If nchop == 1 (and naxis3 == 3) the data unit is both a valid CUBE1
           and a valid CUBE2; Set plane_offset as if the frame type is CUBE2 */
        const int      plane_offset = (naxis3 == 2 * nchop + 1) ? 2 : 3;


        error_if (nchop <= 0, CPL_ERROR_BAD_FILE_FORMAT, "CHOP NCYCLES in %s "
                  "is non-positive (and NAXIS3=%d): %d", file, naxis3, nchop);

        error_if (plane_offset == 3 && naxis3 != nchop+2,
                  CPL_ERROR_BAD_FILE_FORMAT, "NAXIS3=%d and CHOP NCYCLES=%d "
                  "in %s is not a valid VISIR INTERM+Half-Cycle format", naxis3,
                  nchop, file);

        if (plane_offset == 3 && nchop > 1)
            cpl_msg_debug(cpl_func, "%s has %d INTERM-frames and one Half-"
                          "Cycle frame (old CUBE1-format)", file, nchop);

        /* Check the data format */
        sval = visir_pfits_get_frame_type(plist);
        if (sval == NULL) {
            /* Has warned about missing frame type card */
            visir_error_reset("Could not get FITS key");
            /* Don't know whether or not to expect CUBE1 or CUBE2 */
        } else if (strlen(sval) == 0) {
            /* Don't know whether or not to expect CUBE1 or CUBE2 */
        } else if (plane_offset == 3) {
            if (strcmp(sval, "CUBE2")==0)
                cpl_msg_error(cpl_func, "%s has FRAM TYPE = CUBE2, but NAXIS3="
                                "%d and CHOP NCYCLES=%d imply a CUBE1. Assuming"
                                " the frame type is really CUBE1",  file,
                                naxis3, nchop);
        } else if (nchop > 1) {
            /* if nchop == 1 format can be CUBE1 or CUBE2 */
            if (strcmp(sval, "CUBE1")==0)
                cpl_msg_error(cpl_func, "%s has FRAM TYPE = CUBE1, but NAXIS3="
                                "%d and CHOP NCYCLES=%d imply a CUBE2. Assuming"
                                "the frame type is really CUBE2",  file,
                                naxis3, nchop);
        } 

        /* Load last INTERM frame */
        self = cpl_image_load(file, CPL_TYPE_FLOAT, naxis3-plane_offset, 0);

    }

    skip_if (0);

    end_skip;

    if (cpl_error_get_code()) {
        cpl_msg_warning(cpl_func, "Could not load the last INTERM frame from: "
                        "%s", file ? file : "<NULL>");
        cpl_image_delete(self);
        self = NULL;
    }

    return self;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Convert an 'INTERM' image set to the list of Ai-Bi
  @param    self   INTERM imagelist to unpack in place
  @return   CPL_ERROR_NONE, or the relevant CPL_ERROR.
  @note     The function does nothing if self contains just 1 image.

  The input imagelist is expected to be the 'INTERM' image set produced by 
  IRACE. In chopping mode, IRACE accumulates the Ai-Bi differences and 
  produces 'INTERM' image sets like:

  A1-B1
  ((A1-B1) + (A2-B2)) / 2
  ((A1-B1) + (A2-B2) + (A3-B3)) / 3
  etc...

  This function restores the input image set to:

  A1-B1
  A2-B2
  A3-B3
  etc...
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code visir_imagelist_unpack_interm(cpl_imagelist * self) 
{
    cpl_image * iprev;
    cpl_image * image;
    const int   n = cpl_imagelist_get_size(self);
    int         i;


    skip_if (0);

    if (n == 1) return CPL_ERROR_NONE;

    iprev = cpl_imagelist_get(self, n - 1);

    skip_if (0);

    skip_if (cpl_image_multiply_scalar(iprev, n));

    /* Loop on the images - with first and last iteration peeled off */
    for (i = n-1 ; i > 1 ; i--, iprev = image) {
        image = cpl_imagelist_get(self, i-1);

        skip_if (0);

        skip_if (cpl_image_multiply_scalar(image, i));

        skip_if (cpl_image_subtract(iprev, image));

    }

    image = cpl_imagelist_get(self, 0);

    skip_if (0);

    skip_if (cpl_image_subtract(iprev, image));

    end_skip;

    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Remove the glitches in an image
  @param    glitchy   Image to remove glitches from (MODIFIED)
  @return   CPL_ERROR_NONE, or the relevant CPL_ERROR code.
  @note This functions uses some heuristic constants.

  The noise is glitchy minus filter_median(glitchy).
  This noise is filtered iteratively, only the pixels near the mean value
  are kept. 
  filter_median(glitchy) is then added to the filtered noise image to
  reconstruct the corrected image.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code visir_rem_glitch(cpl_image * glitchy) 
{
    cpl_image * med_filt = NULL;
    cpl_mask  * bpm = NULL;
    cpl_mask  * kernel = cpl_mask_new(3, 3);
    double      mean, stdev;
    double      low_thresh, high_thresh;
    const int   nx = cpl_image_get_size_x(glitchy);
    const int   ny = cpl_image_get_size_y(glitchy);
    int         i;

    /* Some heuristic constants */
    double          factor1 = 3.0;
    double          factor2 = 10.0;
    const int       niterations = 5;
    const double    median_corr = 1.5;

    bug_if (0);

    /* Create the filtering kernel */
    bug_if(cpl_mask_not(kernel));
    
    /* Apply a 3x3 median filter to the input image  */
    med_filt = cpl_image_new(cpl_image_get_size_x(glitchy),
                             cpl_image_get_size_y(glitchy),
                             cpl_image_get_type(glitchy));
    bug_if(med_filt == NULL);
    bug_if(cpl_image_filter_mask(med_filt, glitchy, kernel, CPL_FILTER_MEDIAN,
                                 CPL_BORDER_FILTER));
    cpl_mask_delete(kernel);
    kernel = NULL;

    /* Noise is glitchy - med_filt */
    skip_if (cpl_image_subtract(glitchy, med_filt));

    /* niterations iterations */
    for (i=0 ; i < niterations ; i++) {
        /* Compute mean and stdev */
        mean = cpl_image_get_mean(glitchy);
        stdev = cpl_image_get_stdev(glitchy);
        
        skip_if (0);

        /* Set the thresholds */
        low_thresh  = mean - factor1 * stdev;
        high_thresh = mean + factor1 * stdev;

        /* Identify where mean-factor1*stdev < glitchy < mean+factor1*stdev */
        bpm = cpl_mask_threshold_image_create(glitchy,low_thresh,high_thresh);
        skip_if (cpl_mask_not(bpm));
        skip_if (cpl_image_reject_from_mask(glitchy, bpm));
        cpl_mask_delete(bpm);
        bpm = NULL;

        /* Test the number of bad pixels */
        skip_if (cpl_image_count_rejected(glitchy) == nx*ny);
    }

    /* Last iteration */
    /* Compute mean and stdev */
    mean = cpl_image_get_mean(glitchy);
    stdev = cpl_image_get_stdev(glitchy) * median_corr;

    skip_if (0);

    low_thresh  = mean - factor2 * stdev;
    high_thresh = mean + factor2 * stdev;

    bpm = cpl_mask_threshold_image_create(glitchy, low_thresh, high_thresh);
    skip_if (cpl_mask_not(bpm));
    skip_if (cpl_image_reject_from_mask(glitchy, bpm));
    cpl_mask_delete(bpm);
    bpm = NULL;

    /* Test the number of bad pixels */
    skip_if (cpl_image_count_rejected(glitchy) == nx*ny);
    
    /* Set the bad pixels to 0 */
    skip_if (cpl_image_fill_rejected(glitchy, 0.0));
    skip_if (cpl_image_accept_all(glitchy));

    /* Reconstruct the corrected image */
    skip_if (cpl_image_add(glitchy, med_filt));

    end_skip;

    cpl_image_delete(med_filt);
    cpl_mask_delete(bpm);
    cpl_mask_delete(kernel);
    
    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Remove the bad images from an image set
  @param    in      input image iset (MODIFIED)
  @return   0 iff ok

  The input image set is purged from his bad planes
  A bad plane is a plane whose median and stdev values are very different from
  the ones of the other planes.
  If the images set contains less than 3 planes, the function does nothing
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code visir_rem_bad_images(cpl_imagelist * in) 
{
    cpl_vector * medians = NULL;
    cpl_vector * stdevs = NULL;
    cpl_vector * selection = NULL;
    double       mean_medians, mean_stdevs, stdev_medians, stdev_stdevs;
    const double threshold = 3;
    const int    nima = cpl_imagelist_get_size(in);
    int          i;


    /* This will catch a NULL input */
    skip_if (0);

    if (nima <= 3) return CPL_ERROR_NONE;
    
    /* Create medians and stdevs arrays */
    medians = cpl_vector_new(nima);
    stdevs = cpl_vector_new(nima);

    /* Compute the statistics */
    for (i=0 ; i < nima ; i++) {
        cpl_stats * stats = cpl_stats_new_from_image(cpl_imagelist_get(in, i),
                                            CPL_STATS_STDEV | CPL_STATS_MEDIAN);

        cpl_vector_set(medians, i, cpl_stats_get_median(stats));
        cpl_vector_set(stdevs,  i, cpl_stats_get_stdev(stats));
        cpl_stats_delete(stats); /* :-( */
    }

    skip_if( 0);

    /* Get the stats on these arrays */
    mean_medians  = cpl_vector_get_mean(medians);
    stdev_medians = cpl_vector_get_stdev(medians);
    mean_stdevs   = cpl_vector_get_mean(stdevs);
    stdev_stdevs  = cpl_vector_get_stdev(stdevs);

    skip_if (cpl_vector_subtract_scalar(medians, mean_medians));
    skip_if (cpl_vector_subtract_scalar(stdevs,  mean_stdevs));

    stdev_medians *= threshold;
    stdev_stdevs  *= threshold;

    /* Create the selection vector */
    selection = cpl_vector_new(nima);
    skip_if( cpl_vector_fill(selection, 0)); /* Flag all as good */
    for (i=0 ; i < nima ; i++) {
        if (fabs(cpl_vector_get(medians, i)) <= stdev_medians &&
            fabs(cpl_vector_get(stdevs,  i)) <= stdev_stdevs) continue;

        cpl_vector_set(selection, i, -1);
        cpl_msg_info(cpl_func, "Image %d of %d rejected: median=%g, stdev=%g",
                     i+1, nima, stdev_medians, stdev_stdevs);
    }

    /* Purge the bad images in the images set */
    cpl_imagelist_erase(in, selection);

    end_skip;

    cpl_vector_delete(medians);
    cpl_vector_delete(stdevs);
    
    cpl_vector_delete(selection);

    return CPL_ERROR_NONE;

}

/*----------------------------------------------------------------------------*/
/**
  @brief    Correct the offset in a HCYCLE image
  @param    the HCYCLE image
  @return   CPL_ERROR_NONE, or the relevant CPL_ERROR.

  This function adds VISIR_HCYCLE_OFFSET to all pixels in the image

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code visir_offset_hcycle(cpl_image * hcycle) 
{
    double minval;


    skip_if (0);

    skip_if (cpl_image_add_scalar(hcycle, VISIR_HCYCLE_OFFSET));

    minval = cpl_image_get_min(hcycle);

    /* It seems that the offset really is VISIR_HCYCLE_OFFSET-1, warn if not */
    if (minval < 1) cpl_msg_warning(cpl_func, "HCycle pixel minval: %g", minval);

    end_skip;

    return CPL_ERROR_NONE;
}

/*----------------------------------------------------------------------------*/
/**
  @brief   Collapse the 4 beams of a combined image
  @param   qclist  Append QC parameters here
  @param   self    The combined image to collapse (double or float)
  @param   inverse The inverse of the combined image to collapse
  @param   eccmax  Maximum allowed eccentricity (non-positive for any)
  @param   pthrow  Chopping throw [pixel]
  @param   plist   Propertylist of the 1st raw image
  @return  A pointer to the newly allocated image array or NULL on error
  @see     visir_img_collapse_beam()
 */
/*----------------------------------------------------------------------------*/
static
cpl_image ** visir_img_collapse_beam_four(cpl_propertylist       * qclist,
                                          const cpl_image        * self,
                                          const cpl_image        * inverse,
                                          double                   eccmax,
                                          double                   pthrow,
                                          const cpl_propertylist * plist)
{

    cpl_image    ** combined = NULL;
    const int       nx = cpl_image_get_size_x(self);
    const int       ny = cpl_image_get_size_y(self);
    const cpl_type  type = cpl_image_get_type(self);
    cpl_imagelist * list4 = cpl_imagelist_new();
    cpl_image     * swrap = type == CPL_TYPE_DOUBLE
        ? cpl_image_wrap_double(nx, ny, cpl_image_get_data((cpl_image*)self))
        : cpl_image_wrap_float(nx, ny, cpl_image_get_data((cpl_image*)self));
    cpl_image     * iwrap = type == CPL_TYPE_DOUBLE
        ? cpl_image_wrap_double(nx, ny, cpl_image_get_data((cpl_image*)inverse))
        : cpl_image_wrap_float(nx, ny, cpl_image_get_data((cpl_image*)inverse));
    cpl_bivector  * offs = cpl_bivector_new(4);
    double * x4 = cpl_bivector_get_x_data(offs);
    double * y4 = cpl_bivector_get_y_data(offs);
    double pos_x, pos_y;
    int i;

    skip_if (0);

    skip_if(plist  == NULL);

    skip_if(visir_img_find_beam_four(qclist, self, inverse, eccmax, pthrow,
                                     x4, y4));

    /* Combine the four beams */
    for (i = 1; i < 4; i++) {
        x4[i] = x4[0] - x4[i];
        y4[i] = y4[0] - y4[i];
    }

    bug_if (cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM XPOS",
                                           x4[0]));
    bug_if (cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM YPOS",
                                           y4[0]));
    x4[0] = y4[0] = 0.0;

    bug_if(cpl_imagelist_set(list4, (cpl_image*)self,    0));
    bug_if(cpl_imagelist_set(list4, swrap,   1));
    bug_if(cpl_imagelist_set(list4, (cpl_image*)inverse, 2));
    bug_if(cpl_imagelist_set(list4, iwrap,   3));

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

    skip_if(combined == NULL);

    end_skip;

    cpl_bivector_delete(offs);
    for (i = cpl_imagelist_get_size(list4) - 1; i >= 0; i--) {
        (void)cpl_imagelist_unset(list4, i);
    }
    cpl_imagelist_delete(list4);
    (void)cpl_image_unwrap(swrap);
    (void)cpl_image_unwrap(iwrap);
    if (cpl_error_get_code() && combined != NULL) {
        cpl_image_delete(combined[0]);
        cpl_image_delete(combined[1]);
        cpl_free(combined);
    }

    return combined;
}

/*----------------------------------------------------------------------------*/
/**
  @brief  Find the 4 beams in a combined image
  @param  qclist  Append QC parameters here
  @param  self    The image with the positive beam(s) to collapse
  @param  inverse The inverse of the image with the negative beams to collapse
  @param  plist      Propertylist of the 1st raw image
  @param  parlist    Parameterlist w. maximum allowed eccentricity
  @param  recipename The recipe name in the parameterlist
  @param  x4      The X-position [pixel] of the three or four objects
  @param  y4      The Y-position [pixel] of the three or four objects
  @return The beam-mode (VISIR_CHOPNOD_PERPENDICULAR, VISIR_CHOPNOD_PARALLEL)
          or VISIR_CHOPNOD_AUTO on failure.
*/
/*----------------------------------------------------------------------------*/
visir_chopnod_mode visir_img_find_beam(cpl_propertylist        * qclist,
                                       const cpl_image         * self,
                                       const cpl_image         * inverse,
                                       const cpl_propertylist  * plist,
                                       const cpl_parameterlist * parlist,
                                       const char              * recipename,
                                       double                    x4[],
                                       double                    y4[])
{

    cpl_errorstate cleanstate = cpl_errorstate_get();
    visir_chopnod_mode mode = VISIR_CHOPNOD_AUTO; /* Assume failure */

    const double eccmax   = visir_parameterlist_get_double(parlist, recipename,
                                                           VISIR_PARAM_ECCMAX);

    const char   * sdir;

    /* Get the chopping throw in pixels */
    const char * sscale       = visir_pfits_get_pixscale(plist);
    const double pscale       = sscale ? atof(sscale) : 0.0;
    const double pthrow       = pscale > 0.0
        ? visir_pfits_get_chop_throw(plist) / pscale : 0.0;


    skip_if(x4         == NULL);
    skip_if(y4         == NULL);
    skip_if(sscale     == NULL);
    skip_if(self       == NULL);
    skip_if(inverse    == NULL);
    skip_if(parlist    == NULL);
    skip_if(recipename == NULL);
    skip_if(qclist     == NULL);

    sdir = visir_pfits_get_chopnod_dir(plist);

    if (sdir != NULL && !strcmp(sdir, "PERPENDICULAR")) {

        skip_if (visir_img_find_beam_four(qclist, self, inverse, eccmax,
                                          pthrow, x4, y4));

        /* 4 sources - expected and detected */
        mode = VISIR_CHOPNOD_PERPENDICULAR;

    } else if (sdir != NULL && !strcmp(sdir, "PARALLEL")) {

        skip_if (visir_img_find_beam_three(qclist, self, inverse, eccmax,
                                           pthrow, x4, y4));

        /* 3 sources - expected and detected */
        mode = VISIR_CHOPNOD_PARALLEL;

    } else {
        if (sdir == NULL) {
            visir_error_reset("Could not get FITS key");
        } else {
            cpl_msg_warning(cpl_func, "Unknown chopping direction: %s",
                            sdir);
        }
        cpl_msg_warning(cpl_func, "Proceeding as if FITS card "
                        VISIR_PFITS_STRING_CHOPNOD_DIR " had value: %s",
                        "PERPENDICULAR");

        if (visir_img_find_beam_four(qclist, self, inverse, eccmax,
                                     pthrow, x4, y4)) {

            visir_error_reset("Proceeding as if FITS card "
                              VISIR_PFITS_STRING_CHOPNOD_DIR
                              " had value: %s", "PARALLEL");

            skip_if (visir_img_find_beam_three(qclist, self, inverse,
                                               eccmax, pthrow, x4, y4));

            /* 3 sources - unexpected, but detected */
            mode = VISIR_CHOPNOD_PARALLEL;

        } else {
            /* 4 sources - unexpected, but detected */
            mode = VISIR_CHOPNOD_PERPENDICULAR;
        }
    }

    bug_if (cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM THROW",
                                           pthrow));
    bug_if (cpl_propertylist_set_comment(qclist, "ESO QC ONEBEAM THROW",
                                         "The throw in pixels (TEL CHOP THROW "
                                         "divided by INS PFOV)"));
    end_skip;

    return mode;
}


/*----------------------------------------------------------------------------*/
/**
  @brief   Find the 4 beams in a combined image
  @param   qclist  Append QC parameters here
  @param   self    The combined image to collapse
  @param   inverse The inverse of the combined image to collapse
  @param   eccmax  Maximum allowed eccentricity (non-positive for any)
  @param   pthrow  Chopping throw [pixel]
  @param   x4      The X-position [pixel] of the four objects
  @param   y4      The Y-position [pixel] of the four objects
  @return  CPL_ERROR_NONE or the relevant CPL error code on error
 */
/*----------------------------------------------------------------------------*/
static
cpl_error_code visir_img_find_beam_four(cpl_propertylist * qclist,
                                        const cpl_image  * self,
                                        const cpl_image  * inverse,
                                        double             eccmax,
                                        double             pthrow,
                                        double             x4[],
                                        double             y4[])
{

    cpl_errorstate  cleanstate = cpl_errorstate_get();
    cpl_apertures * appos = NULL;
    cpl_apertures * apneg = NULL;
    const double    psigmas[] = {2.0, 1.0, 0.5};
    const int       nsigmas = sizeof(psigmas)/sizeof(double);
    int             isigma;
    int             iappos2[] = {1, 2};
    int             iapneg2[] = {1, 2};

    bug_if(0);
    skip_if(self   == NULL);
    skip_if(qclist == NULL);
    skip_if(pthrow <= 0.0);
    skip_if(x4     == NULL);
    skip_if(y4     == NULL);

    cpl_msg_info(cpl_func, "Detecting the 4-beam object with %g pixel throw "
                 "using %d sigma-levels ranging from %g down to %g", pthrow,
                 nsigmas, psigmas[0], psigmas[nsigmas-1]);

    bug_if(0);
    for (isigma = 0; isigma < nsigmas; isigma++) {
        int npos = 0;
        int nneg = 0;

        /* Detect the (two) POSITIVE objects */
        cpl_apertures_delete(appos);
        appos = cpl_apertures_extract_sigma(self, psigmas[isigma]);

        if (appos != NULL) {
            npos = cpl_apertures_get_size(appos);
        }


        /* Detect the (two) NEGATIVE objects */
        cpl_apertures_delete(apneg);
        apneg = cpl_apertures_extract_sigma(inverse, psigmas[isigma]);
        if (apneg != NULL) {
            nneg = cpl_apertures_get_size(apneg);
        }

        cpl_msg_info(cpl_func, "Found %d positive (need 2) and %d negative "
                     "(need 2) object(s) at sigma=%g (%d of %d)", npos, nneg,
                     psigmas[isigma], 1+isigma, nsigmas);

        if (eccmax > 0.0) {
            int ipos1;
            double eccbest = eccmax;
            double eccmin  = DBL_MAX;
            double fluxbest = 0.0;
            double fluxecc = DBL_MAX;
            cpl_boolean is_first = CPL_TRUE;

#ifdef _OPENMP
#pragma omp parallel for private(ipos1)
#endif
            for (ipos1 = 2; ipos1 <= npos; ipos1++) {
                int ipos2, ineg1, ineg2;
                for (ipos2 = 1; ipos2 < ipos1; ipos2++) {
                    for (ineg1 = 2; ineg1 <= nneg; ineg1++) {
                        for (ineg2 = 1; ineg2 < ineg1; ineg2++) {
                            cpl_boolean swappos, swapneg;
                            const double ecc
                                = visir_img_check_box(appos, ipos1, ipos2,
                                                      apneg, ineg1, ineg2,
                                                      pthrow, &swappos,
                                                      &swapneg);

                            const double flux
                                = cpl_apertures_get_flux(appos, ipos1)
                                + cpl_apertures_get_flux(appos, ipos2)
                                + cpl_apertures_get_flux(apneg, ineg1)
                                + cpl_apertures_get_flux(apneg, ineg2);


                            if (ecc < 0.0 || flux <= 0.0 ||
                                !cpl_errorstate_is_equal(cleanstate)) {
                                irplib_error_recover(cleanstate, "Invalid 4-"
                                                     "object (%d & %d of %d, "
                                                     "%d & %d of %d)",
                                                     ipos2, ipos1, npos,
                                                     ineg2, ineg1, nneg);
                                continue;
                            }

#ifdef _OPENMP
#pragma omp critical(visir_img_find_beam_four_min)
#endif
                            if (ecc < eccmin)
                            {
                                eccmin = ecc;
                                fluxecc = flux;
                            }

                            if (eccmax <= ecc) continue;

#ifdef _OPENMP
#pragma omp critical(visir_img_find_beam_four_ok)
#endif
                            if (is_first || ecc * fluxbest < eccbest * flux)
                                {
                                if (is_first) {
                                    is_first = CPL_FALSE;
                                    cpl_msg_info(cpl_func, "Found 4 object "
                                                 "positions with throw-"
                                                 "scaled eccentricity %g "
                                                 "and flux %g", ecc, flux);
                                } else {
                                    cpl_msg_info(cpl_func, "Found 4 object "
                                                 "positions with throw-"
                                                 "scaled eccentricity %g "
                                                 "< %g and/or flux %g > %g",
                                                 ecc, eccbest, flux, fluxbest);
                                }
                                eccbest = ecc;
                                fluxbest = flux;
                                iappos2[0] = swappos ? ipos2 : ipos1;
                                iappos2[1] = swappos ? ipos1 : ipos2;
                                iapneg2[0] = swapneg ? ineg2 : ineg1;
                                iapneg2[1] = swapneg ? ineg1 : ineg2;
                            }
                        }
                    }
                }
            }
            if (eccbest < eccmax) {
                bug_if(cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM "
                                                      "ECCENTRICITY", eccbest));

                break;
            }
            if (eccmin < DBL_MAX) {
                cpl_msg_info(cpl_func, "Found 4 sigma-%g object positions with "
                             "too large throw-scaled eccentricity %g >= %g and "
                             "flux %g", psigmas[isigma], eccmin, eccmax,
                             fluxecc);
            }
        } else if (npos >= 2 && nneg >= 2) {
            cpl_apertures_sort_by_flux(appos);
            cpl_apertures_sort_by_flux(apneg);
            break;
        }

        if (isigma + 1 < nsigmas) {
            irplib_error_recover(cleanstate, "4-Beam positions not found among "
                                 "%d postive and %d negative object(s) at "
                                 "sigma=%g, (%d of %d)", npos, nneg,
                                 psigmas[isigma], 1+isigma, nsigmas);
        }
    }

    error_if (isigma == nsigmas, CPL_ERROR_DATA_NOT_FOUND,
              "4-Beam positions not found w. %d sigma(s) down to %g",
              nsigmas, psigmas[nsigmas - 1]);

    if (cpl_msg_get_level() == CPL_MSG_DEBUG) {
        cpl_apertures_dump(appos, stdout);
        cpl_apertures_dump(apneg, stdout);
    }

    x4[0] = cpl_apertures_get_centroid_x(appos, iappos2[0]);
    y4[0] = cpl_apertures_get_centroid_y(appos, iappos2[0]);
    x4[1] = cpl_apertures_get_centroid_x(appos, iappos2[1]);
    y4[1] = cpl_apertures_get_centroid_y(appos, iappos2[1]);

    x4[2] = cpl_apertures_get_centroid_x(apneg, iapneg2[0]);
    y4[2] = cpl_apertures_get_centroid_y(apneg, iapneg2[0]);
    x4[3] = cpl_apertures_get_centroid_x(apneg, iapneg2[1]);
    y4[3] = cpl_apertures_get_centroid_y(apneg, iapneg2[1]);

    bug_if(cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM FLUX",
                                          cpl_apertures_get_flux(appos,
                                                                 iappos2[0])));

    cpl_msg_info(cpl_func, "Centroid of positive object 1 [pixel]: %g %g",
                 x4[0], y4[0]);
    cpl_msg_info(cpl_func, "Centroid of positive object 2 [pixel]: %g %g",
                 x4[1], y4[1]);

    cpl_msg_info(cpl_func, "Centroid of negative object 1 [pixel]: %g %g",
                 x4[2], y4[2]);
    cpl_msg_info(cpl_func, "Centroid of negative object 2 [pixel]: %g %g",
                 x4[3], y4[3]);

    cpl_msg_info(cpl_func, "Expected object distance (chop throw) [pixel]: %g",
                 pthrow);
    cpl_msg_info(cpl_func, "Object Pos -> Pos x/y-distance [pixel]: %g %g",
                 x4[1] - x4[0], y4[1] - y4[0]);
    cpl_msg_info(cpl_func, "Object Neg -> Neg x/y-distance [pixel]: %g %g",
                 x4[3] - x4[2], y4[3] - y4[2]);
    cpl_msg_info(cpl_func, "Object Pos -> Pos angle [degrees]: %g",
                 atan2(y4[1] - y4[0], x4[1] - x4[0]) * CPL_MATH_DEG_RAD);
    cpl_msg_info(cpl_func, "Object Neg -> Neg angle [degrees]: %g",
                 atan2(y4[3] - y4[2], x4[3] - x4[2]) * CPL_MATH_DEG_RAD);

    end_skip;

    cpl_apertures_delete(appos);
    cpl_apertures_delete(apneg);

    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief   Collapse the 3 beams of a combined image
  @param   qclist  Append QC parameters here
  @param   self    The combined image to collapse
  @param   inverse The inverse of the combined image to collapse
  @param   eccmax  Maximum allowed eccentricity (non-positive for any)
  @param   pthrow  Chopping throw [pixel]
  @param   plist   Propertylist of the 1st raw image
  @return  A pointer to the newly allocated image array or NULL on error
  @see    visir_img_collapse_beam()
 */
/*----------------------------------------------------------------------------*/
static
cpl_image ** visir_img_collapse_beam_three(cpl_propertylist       * qclist,
                                           const cpl_image        * self,
                                           const cpl_image        * inverse,
                                           double                   eccmax,
                                           double                   pthrow,
                                           const cpl_propertylist * plist)
{

    cpl_image    ** combined = NULL;
    const int       nx = cpl_image_get_size_x(self);
    const int       ny = cpl_image_get_size_y(self);
    const cpl_type  type = cpl_image_get_type(self);
    cpl_imagelist * list3 = cpl_imagelist_new();
    cpl_image     * iwrap = type == CPL_TYPE_DOUBLE
        ? cpl_image_wrap_double(nx, ny, cpl_image_get_data((cpl_image*)inverse))
        : cpl_image_wrap_float(nx, ny, cpl_image_get_data((cpl_image*)inverse));
    cpl_bivector  * offs = cpl_bivector_new(3);
    double * x3 = cpl_bivector_get_x_data(offs);
    double * y3 = cpl_bivector_get_y_data(offs);
    double pos_x, pos_y;
    int i;

    skip_if (0);

    skip_if(plist  == NULL);

    skip_if(visir_img_find_beam_three(qclist, self, inverse, eccmax, pthrow,
                                      x3, y3));

    /* Combine the four beams */
    for (i = 1; i < 3; i++) {
        x3[i] = x3[0] - x3[i];
        y3[i] = y3[0] - y3[i];
    }
    bug_if (cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM XPOS",
                                           x3[0]));
    bug_if (cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM YPOS",
                                           y3[0]));

    x3[0] = y3[0] = 0.0;

    bug_if(cpl_imagelist_set(list3, (cpl_image*)self,    0));
    bug_if(cpl_imagelist_set(list3, (cpl_image*)inverse, 1));
    bug_if(cpl_imagelist_set(list3, iwrap,   2));

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

    skip_if(combined == NULL);

    end_skip;

    cpl_bivector_delete(offs);
    for (i = cpl_imagelist_get_size(list3) - 1; i >= 0; i--) {
        (void)cpl_imagelist_unset(list3, i);
    }
    cpl_imagelist_delete(list3);
    (void)cpl_image_unwrap(iwrap);
    if (cpl_error_get_code() && combined != NULL) {
        cpl_image_delete(combined[0]);
        cpl_image_delete(combined[1]);
        cpl_free(combined);
    }

    return combined;
}


/*----------------------------------------------------------------------------*/
/**
  @brief   Find the 3 beams in a combined image
  @param   qclist  Append QC parameters here
  @param   self    The combined image to collapse
  @param   inverse The inverse of the combined image to collapse
  @param   eccmax  Maximum allowed eccentricity (non-positive for any)
  @param   pthrow  Chopping throw [pixel]
  @param   x3      The X-position [pixel] of the three objects
  @param   y3      The Y-position [pixel] of the three objects
  @return  CPL_ERROR_NONE or the relevant CPL error code on error
  @note FIXME: Modified from visir_img_find_beam_four()
 */
/*----------------------------------------------------------------------------*/
static
cpl_error_code visir_img_find_beam_three(cpl_propertylist * qclist,
                                        const cpl_image  * self,
                                        const cpl_image  * inverse,
                                        double             eccmax,
                                        double             pthrow,
                                        double             x3[],
                                        double             y3[])
{

    cpl_errorstate  cleanstate = cpl_errorstate_get();
    cpl_apertures * appos = NULL;
    cpl_apertures * apneg = NULL;
    const double    psigmas[] = {2.0, 1.0, 0.5};
    const int       nsigmas = sizeof(psigmas)/sizeof(double);
    int             isigma;
    int             iappos [] = {1};
    int             iapneg2[] = {1, 2};

    bug_if(0);
    skip_if(self   == NULL);
    skip_if(qclist == NULL);
    skip_if(pthrow <= 0.0);
    skip_if(eccmax <  0.0);
    skip_if(x3     == NULL);
    skip_if(y3     == NULL);


    cpl_msg_info(cpl_func, "Detecting the 3-beam object with %g pixel throw "
                 "using %d sigma-levels ranging from %g down to %g", pthrow,
                 nsigmas, psigmas[0], psigmas[nsigmas-1]);

    bug_if(0);
    for (isigma = 0; isigma < nsigmas; isigma++) {
        int npos = 0;
        int nneg = 0;

        /* Detect the (single) POSITIVE object */
        cpl_apertures_delete(appos);
        appos = cpl_apertures_extract_sigma(self, psigmas[isigma]);

        if (appos != NULL) {
            npos = cpl_apertures_get_size(appos);
        }


        /* Detect the (two) NEGATIVE objects */
        cpl_apertures_delete(apneg);
        apneg = cpl_apertures_extract_sigma(inverse, psigmas[isigma]);
        if (apneg != NULL) {
            nneg = cpl_apertures_get_size(apneg);
        }

        cpl_msg_info(cpl_func, "Found %d positive (need 1) and %d negative "
                     "(need 2) object(s) at sigma=%g (%d of %d)", npos, nneg,
                     psigmas[isigma], 1+isigma, nsigmas);

        if (eccmax > 0.0) {
            int ipos;
            double eccbest = eccmax;
            double eccmin  = DBL_MAX;
            double fluxbest = 0.0;
            double fluxecc = DBL_MAX;
            cpl_boolean is_first = CPL_TRUE;

#ifdef _OPENMP
#pragma omp parallel for private(ipos)
#endif
            for (ipos = 1; ipos <= npos; ipos++) {
                int ineg1, ineg2;
                for (ineg1 = 2; ineg1 <= nneg; ineg1++) {
                    for (ineg2 = 1; ineg2 < ineg1; ineg2++) {
                        cpl_boolean swapnegh, swapnegv;
                        /* Find best of horizontal/vertical ecc */
                        const double ecch
                            = visir_img_check_align(appos, ipos, apneg, ineg1,
                                                    ineg2, pthrow, CPL_TRUE,
                                                    &swapnegh);
                        const double eccv
                            = visir_img_check_align(appos, ipos, apneg, ineg1,
                                                    ineg2, pthrow, CPL_FALSE,
                                                    &swapnegv);
                        const double ecc = ecch < eccv ? ecch : eccv;
                        const cpl_boolean swapneg = ecch < eccv
                            ? swapnegh : swapnegv;

                        const double flux
                            = cpl_apertures_get_flux(appos, ipos)
                            + cpl_apertures_get_flux(apneg, ineg1)
                            + cpl_apertures_get_flux(apneg, ineg2);


                        if (ecc < 0.0 || flux <= 0.0 ||
                            !cpl_errorstate_is_equal(cleanstate)) {
                            irplib_error_recover(cleanstate, "Invalid 3-"
                                                 "object (%d of %d, "
                                                 "%d & %d of %d)",
                                                 ipos, npos,
                                                 ineg2, ineg1, nneg);
                            continue;
                        }

#ifdef _OPENMP
#pragma omp critical(visir_img_collapse_beam_three_min)
#endif
                        if (ecc < eccmin)
                        {
                            eccmin = ecc;
                            fluxecc = flux;
                        }

                        if (eccmax <= ecc) continue;

#ifdef _OPENMP
#pragma omp critical(visir_img_collapse_beam_three_ok)
#endif
                        if (is_first || ecc * fluxbest < eccbest * flux)
                        {
                            if (is_first) {
                                is_first = CPL_FALSE;
                                cpl_msg_info(cpl_func, "Found 3 object posi"
                                             "tions with throw-scaled (ver"
                                             "tical/horizontal) eccentrici"
                                             "ty %g/%g and flux %g", eccv,
                                             ecch, flux);
                            } else {
                                cpl_msg_info(cpl_func, "Found 3 object posi"
                                             "tions with throw-scaled (ver"
                                             "tical/horizontal) eccentrici"
                                             "ty %g/%g < %g and/or flux %g "
                                             "> %g", eccv, ecch, eccbest,
                                             flux, fluxbest);
                            }
                            eccbest = ecc;
                            fluxbest = flux;
                            iappos[0] = ipos;
                            iapneg2[0] = swapneg ? ineg2 : ineg1;
                            iapneg2[1] = swapneg ? ineg1 : ineg2;
                        }
                    }
                }
            }
            if (eccbest < eccmax) {
                bug_if(cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM "
                                                      "ECCENTRICITY", eccbest));
                break;
            }
            if (eccmin < DBL_MAX) {
                cpl_msg_info(cpl_func, "Found 3 sigma-%g object positions with "
                             "too large throw-scaled eccentricity %g >= %g and "
                             "flux %g", psigmas[isigma], eccmin, eccmax,
                             fluxecc);
            }
        } else if (npos >= 1 && nneg >= 2) {
            cpl_apertures_sort_by_flux(appos);
            cpl_apertures_sort_by_flux(apneg);
            break;
        }

        if (isigma + 1 < nsigmas) {
            irplib_error_recover(cleanstate, "3-Beam positions not found among "
                                 "%d postive and %d negative object(s) at "
                                 "sigma=%g, (%d of %d)", npos, nneg,
                                 psigmas[isigma], 1+isigma, nsigmas);
        }
    }

    error_if (isigma == nsigmas, CPL_ERROR_DATA_NOT_FOUND,
              "3-Beam positions not found w. %d sigma(s) down to %g",
              nsigmas, psigmas[nsigmas - 1]);

    if (cpl_msg_get_level() == CPL_MSG_DEBUG) {
        cpl_apertures_dump(appos, stdout);
        cpl_apertures_dump(apneg, stdout);
    }

    x3[0] = cpl_apertures_get_centroid_x(appos, iappos[0]);
    y3[0] = cpl_apertures_get_centroid_y(appos, iappos[0]);

    x3[1] = cpl_apertures_get_centroid_x(apneg, iapneg2[0]);
    y3[1] = cpl_apertures_get_centroid_y(apneg, iapneg2[0]);
    x3[2] = cpl_apertures_get_centroid_x(apneg, iapneg2[1]);
    y3[2] = cpl_apertures_get_centroid_y(apneg, iapneg2[1]);

    bug_if(cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM FLUX",
                                          cpl_apertures_get_flux(appos,
                                                                 iappos[0])));

    cpl_msg_info(cpl_func, "Centroid of positive object [pixel]: %g %g",
                 x3[0], y3[0]);

    cpl_msg_info(cpl_func, "Centroid of negative object 1 [pixel]: %g %g",
                 x3[1], y3[1]);
    cpl_msg_info(cpl_func, "Centroid of negative object 2 [pixel]: %g %g",
                 x3[2], y3[2]);

    cpl_msg_info(cpl_func, "Expected object distance (chop throw) [pixel]: %g",
                 pthrow);
    cpl_msg_info(cpl_func, "Object Neg1 -> Pos x/y-distance [pixel]: %g %g",
                 x3[2] - x3[0], y3[2] - y3[0]);
    cpl_msg_info(cpl_func, "Object Pos -> Neg2 x/y-distance [pixel]: %g %g",
                 x3[0] - x3[1], y3[0] - y3[1]);

    end_skip;

    cpl_apertures_delete(appos);
    cpl_apertures_delete(apneg);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief   Find the 2 beams in a nodded image
  @param   qclist  Append QC parameters here
  @param   self    The nodded image
  @param   inverse The inverse of the nodded image
  @param   eccmax  Maximum allowed eccentricity (non-positive for any)
  @param   pthrow  Chopping throw [pixel] with sign from pos to neg (neg - pos)
  @param   x2      The X-position [pixel] of the (positive + negative) objects
  @param   y2      The Y-position [pixel] of the (positive + negative) objects
  @return  CPL_ERROR_NONE or the relevant CPL error code on error
  @note FIXME: Modified from visir_img_find_beam_three()

 */
/*----------------------------------------------------------------------------*/
cpl_error_code visir_img_find_beam_two(cpl_propertylist * qclist,
                                       const cpl_image  * self,
                                       const cpl_image  * inverse,
                                       double             eccmax,
                                       double             pthrow,
                                       double             x2[],
                                       double             y2[])
{

    cpl_errorstate  cleanstate = cpl_errorstate_get();
    cpl_apertures * appos = NULL;
    cpl_apertures * apneg = NULL;
    const double    psigmas[] = {2.0, 1.0, 0.5};
    const int       nsigmas = sizeof(psigmas)/sizeof(double);
    int             isigma;
    int             iappos[] = {1};
    int             iapneg[] = {1};

    bug_if(0);
    skip_if(self   == NULL);
    skip_if(qclist == NULL);
    skip_if(eccmax <  0.0);
    skip_if(x2     == NULL);
    skip_if(y2     == NULL);

    if (pthrow > 0.0) {
        cpl_msg_info(cpl_func, "Detecting the 2-beam object (Pos -> Neg) with "
                     "%g pixel throw using %d sigma-levels ranging from %g down"
                     " to %g", pthrow, nsigmas, psigmas[0], psigmas[nsigmas-1]);
    } else if (pthrow < 0.0) {
        cpl_msg_info(cpl_func, "Detecting the 2-beam object (Neg -> Pos) with "
                     "%g pixel throw using %d sigma-levels ranging from %g down"
                     " to %g", pthrow, nsigmas, psigmas[0], psigmas[nsigmas-1]);
    } else {
        skip_if(1);
    }

    bug_if(0);
    for (isigma = 0; isigma < nsigmas; isigma++) {
        int npos = 0;
        int nneg = 0;

        /* Detect the (single) POSITIVE object */
        cpl_apertures_delete(appos);
        appos = cpl_apertures_extract_sigma(self, psigmas[isigma]);

        if (appos != NULL) {
            npos = cpl_apertures_get_size(appos);
        }


        /* Detect the NEGATIVE object */
        cpl_apertures_delete(apneg);
        apneg = cpl_apertures_extract_sigma(inverse, psigmas[isigma]);
        if (apneg != NULL) {
            nneg = cpl_apertures_get_size(apneg);
        }

        cpl_msg_info(cpl_func, "Found %d positive (need 1) and %d negative "
                     "(need 1) object(s) at sigma=%g (%d of %d)", npos, nneg,
                     psigmas[isigma], 1+isigma, nsigmas);

        if (eccmax > 0.0) {
            int ipos;
            double eccbest = eccmax;
            double eccmin  = DBL_MAX;
            double fluxbest = 0.0;
            double fluxecc = DBL_MAX;
            cpl_boolean is_first = CPL_TRUE;

#ifdef _OPENMP
#pragma omp parallel for private(ipos)
#endif
            for (ipos = 1; ipos <= npos; ipos++) {
                int ineg;
                for (ineg = 1; ineg <= nneg; ineg++) {
                    const double ecc = pthrow > 0.0
                        ? visir_img_check_line(appos, ipos, apneg, ineg,
                                               pthrow, CPL_FALSE)
                        : visir_img_check_line(apneg, ineg, appos, ipos,
                                               -pthrow, CPL_FALSE);

                    const double flux
                        = cpl_apertures_get_flux(appos, ipos)
                        + cpl_apertures_get_flux(apneg, ineg);


                    if (ecc < 0.0 || flux <= 0.0 ||
                        !cpl_errorstate_is_equal(cleanstate)) {
                        irplib_error_recover(cleanstate, "Invalid 2-"
                                             "object (%d of %d, "
                                             "%d of %d)",
                                             ipos, npos,
                                             ineg, nneg);
                        continue;
                    }

#ifdef _OPENMP
#pragma omp critical(visir_img_collapse_beam_two_min)
#endif
                    if (ecc < eccmin)
                        {
                            eccmin = ecc;
                            fluxecc = flux;
                        }

                    if (eccmax <= ecc) continue;

#ifdef _OPENMP
#pragma omp critical(visir_img_collapse_beam_two_ok)
#endif
                    if (is_first || ecc * fluxbest < eccbest * flux)
                        {
                            if (is_first) {
                                is_first = CPL_FALSE;
                                cpl_msg_info(cpl_func, "Found 2 object posi"
                                             "tions with throw-scaled eccen"
                                             "tricity %g and flux %g", ecc,
                                             flux);
                            } else {
                                cpl_msg_info(cpl_func, "Found 2 object posi"
                                             "tions with throw-scaled eccen"
                                             "tricity %g < %g and/or flux %g "
                                             "> %g", ecc, eccbest, flux,
                                             fluxbest);
                            }
                            eccbest = ecc;
                            fluxbest = flux;
                            iappos[0] = ipos;
                            iapneg[0] = ineg;
                        }
                }
            }
            if (eccbest < eccmax) {
                bug_if(cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM "
                                                      "ECCENTRICITY", eccbest));
                break;
            }
            if (eccmin < DBL_MAX) {
                cpl_msg_info(cpl_func, "Found 2 sigma-%g object positions with "
                             "too large throw-scaled eccentricity %g >= %g and "
                             "flux %g", psigmas[isigma], eccmin, eccmax,
                             fluxecc);
            }
        } else if (npos >= 1 && nneg >= 2) {
            cpl_apertures_sort_by_flux(appos);
            cpl_apertures_sort_by_flux(apneg);
            break;
        }

        if (isigma + 1 < nsigmas) {
            irplib_error_recover(cleanstate, "2-Beam positions not found among "
                                 "%d postive and %d negative object(s) at "
                                 "sigma=%g, (%d of %d)", npos, nneg,
                                 psigmas[isigma], 1+isigma, nsigmas);
        }
    }

    error_if (isigma == nsigmas, CPL_ERROR_DATA_NOT_FOUND,
              "2-Beam positions not found w. %d sigma(s) down to %g",
              nsigmas, psigmas[nsigmas - 1]);

    if (cpl_msg_get_level() == CPL_MSG_DEBUG) {
        cpl_apertures_dump(appos, stdout);
        cpl_apertures_dump(apneg, stdout);
    }

    x2[0] = cpl_apertures_get_centroid_x(appos, iappos[0]);
    y2[0] = cpl_apertures_get_centroid_y(appos, iappos[0]);

    x2[1] = cpl_apertures_get_centroid_x(apneg, iapneg[0]);
    y2[1] = cpl_apertures_get_centroid_y(apneg, iapneg[0]);

    bug_if(cpl_propertylist_append_double(qclist, "ESO QC ONEBEAM FLUX",
                                          cpl_apertures_get_flux(appos,
                                                                 iappos[0])));

    cpl_msg_info(cpl_func, "Centroid of positive object [pixel]: %g %g",
                 x2[0], y2[0]);

    cpl_msg_info(cpl_func, "Centroid of negative object [pixel]: %g %g",
                 x2[1], y2[1]);

    if (pthrow > 0.0) {
        cpl_msg_info(cpl_func, "Expected object distance (chop throw) "
                     "[pixel]: %g", pthrow);

        cpl_msg_info(cpl_func, "Object Pos -> Neg x/y-distance [pixel]: %g %g",
                     x2[1] - x2[0], y2[1] - y2[0]);
    } else {
        cpl_msg_info(cpl_func, "Expected object distance (chop throw) "
                     "[pixel]: %g", -pthrow);

        cpl_msg_info(cpl_func, "Object Neg -> x/y-distance [pixel]: %g %g",
                     x2[0] - x2[1], y2[0] - y2[1]);
    }

    end_skip;

    cpl_apertures_delete(appos);
    cpl_apertures_delete(apneg);

    return cpl_error_get_code();
}



/*----------------------------------------------------------------------------*/
/**
  @brief    Compute an eccentricity in pixels of a four-point object
  @param    appos  The apertures of the positive objects
  @param    ipos1  The index of the top left positive object
  @param    ipos2  The index of the bottom right positive object
  @param    apneg  The apertures of the negative objects
  @param    ineg1  The index of the top right negative object
  @param    ineg2  The index of the bottom left negative object
  @param    ssize  Length of one side of the square
  @param    pswapp On success, set to true if ipos1, ipos2 are to be swapped
  @param    pswapn On success, set to true if ineg1, ineg2 are to be swapped
  @return   The non-negative eccentricity, or a negative number on error
  @note     A negative number is returned when called with an existing error

  The eccentricity is zero for a square with vertical and horizontal sides and
  increases as the four points define something less alike that.

 */
/*----------------------------------------------------------------------------*/
double visir_img_check_box(const cpl_apertures * appos,
                           int ipos1, int ipos2,
                           const cpl_apertures * apneg,
                           int ineg1, int ineg2, double ssize,
                           cpl_boolean * pswapp, cpl_boolean * pswapn)
{

    /* NB: Lower left pixel is (1, 1) */

    /* The two positive points */
    const double xp1 = cpl_apertures_get_centroid_x(appos, ipos1);
    const double xp2 = cpl_apertures_get_centroid_x(appos, ipos2);
    const double yp1 = cpl_apertures_get_centroid_y(appos, ipos1);
    const double yp2 = cpl_apertures_get_centroid_y(appos, ipos2);

    /* The leftmost positive point */
    const double xpl = xp1 < xp2 ? xp1 : xp2;
    const double ypl = xp1 < xp2 ? yp1 : yp2;

    /* The rightmost positive point */
    const double xpr = xp1 < xp2 ? xp2 : xp1;
    const double ypr = xp1 < xp2 ? yp2 : yp1;

    /* The two negative points */
    const double xn1 = cpl_apertures_get_centroid_x(apneg, ineg1);
    const double yn1 = cpl_apertures_get_centroid_y(apneg, ineg1);
    const double xn2 = cpl_apertures_get_centroid_x(apneg, ineg2);
    const double yn2 = cpl_apertures_get_centroid_y(apneg, ineg2);

    /* The leftmost negative point */
    const double xnl = xn1 < xn2 ? xn1 : xn2;
    const double ynl = xn1 < xn2 ? yn1 : yn2;

    /* The rightmost negative point */
    const double xnr = xn1 < xn2 ? xn2 : xn1;
    const double ynr = xn1 < xn2 ? yn2 : yn1;

    const double lx1 = xnr - xpl; /* The length of the top x-side */
    const double lx2 = xpr - xnl; /* The length of the bottom x-side */
    const double ly1 = ypl - ynl; /* The length of the left y-side */
    const double ly2 = ynr - ypr; /* The length of the right y-side */

    const double dx1 = lx1 - ssize;
    const double dx2 = lx2 - ssize;
    const double dy1 = ly1 - ssize;
    const double dy2 = ly2 - ssize;

    const double ey1 = ynr - ypl; /* The displacement in the top x-side */
    const double ey2 = ypr - ynl; /* The displacement in the bottom x-side */
    const double ex1 = xpl - xnl; /* The displacement in the left y-side */
    const double ex2 = xpr - xnr; /* The displacement in the right y-side */

    const double ok = sqrt(dx1 * dx1 + dx2 * dx2 + dy1 * dy1 + dy2 * dy2 +
                           ex1 * ex1 + ex2 * ex2 + ey1 * ey1 + ey2 * ey2);

    double result = -1.0; /* Assume failure */

    skip_if(0); /* Catches NULL apertures and illegal index input */

    skip_if(pswapp == NULL);
    skip_if(pswapn == NULL);
    skip_if(appos == apneg);
    skip_if(ipos1 == ipos2);
    skip_if(ineg1 == ineg2);

    skip_if(ssize <= 0.0);

    *pswapp = xp1 < xp2 ? CPL_FALSE : CPL_TRUE;
    *pswapn = xn1 < xn2 ? CPL_FALSE : CPL_TRUE;

    result = ok/ssize; /* OK to divide now */

    end_skip;    

    return result;

}


/*----------------------------------------------------------------------------*/
/**
  @brief    Compute an eccentricity in pixels of a three-point-aligned object
  @param    appos  The apertures of the positive objects
  @param    ipos   The index of the middle, positive object
  @param    apneg  The apertures of the negative objects
  @param    ineg1  The index of the lower/leftmost negative object
  @param    ineg2  The index of the opposite negative object
  @param    ssize  Expected distance from positive to negative objects
  @param    is_hor True iff the alignment is to be horizontal, else vertical
  @param    pswapn On success, set to true if ineg1, ineg2 are to be swapped
  @return   The non-negative eccentricity, or a negative number on error
  @note     A negative number is returned when called with an existing error

  The eccentricity is zero when the two negative points are exactly opposite
  of the positive, in the specified (horizontal/vertical) direction and
  increases as the three points define something less alike that.

 */
/*----------------------------------------------------------------------------*/
double visir_img_check_align(const cpl_apertures * appos, int ipos,
                             const cpl_apertures * apneg, int ineg1, int ineg2,
                             double ssize, cpl_boolean is_hor,
                             cpl_boolean * pswapn)
{

    /* NB: Lower left pixel is (1, 1) */

    /* The positive point */
    const double xp = cpl_apertures_get_centroid_x(appos, ipos);
    const double yp = cpl_apertures_get_centroid_y(appos, ipos);

    /* The two negative points */
    const double xn1 = cpl_apertures_get_centroid_x(apneg, ineg1);
    const double yn1 = cpl_apertures_get_centroid_y(apneg, ineg1);
    const double xn2 = cpl_apertures_get_centroid_x(apneg, ineg2);
    const double yn2 = cpl_apertures_get_centroid_y(apneg, ineg2);

    double result = -1.0; /* Assume failure */

    double ok;

    cpl_boolean swapn;

    if (is_hor) {
        /* The leftmost negative point */
        const double xnl = xn1 < xn2 ? xn1 : xn2;
        const double ynl = xn1 < xn2 ? yn1 : yn2;

        /* The rightmost negative point */
        const double xnr = xn1 < xn2 ? xn2 : xn1;
        const double ynr = xn1 < xn2 ? yn2 : yn1;

        const double d1 = xnr - xp - ssize; /* The rightmost length deviation */
        const double d2 = xp - xnl - ssize; /* The  leftmost length deviation */

        const double e1 = ynr - yp;  /* The rigthmost orthogonal deviation */
        const double e2 = yp - ynl;  /* The leftmost  orthogonal deviation */

        swapn = xn1 < xn2 ? CPL_FALSE : CPL_TRUE;

        ok = sqrt(d1 * d1 + d2 * d2 + e1 * e1 + e2 * e2);

    } else {
        /* The lower negative point */
        const double xnl = yn1 < yn2 ? xn1 : xn2;
        const double ynl = yn1 < yn2 ? yn1 : yn2;

        /* The upper negative point */
        const double xnr = yn1 < yn2 ? xn2 : xn1;
        const double ynr = yn1 < yn2 ? yn2 : yn1;

        const double d1 = ynr - yp - ssize; /* The upper length deviation */
        const double d2 = yp - ynl - ssize; /* The lower length deviation */

        const double e1 = xnr - xp;  /* The upper orthogonal deviation */
        const double e2 = xp - xnl;  /* The lower orthogonal deviation */

        swapn = yn1 < yn2 ? CPL_FALSE : CPL_TRUE;

        ok = sqrt(d1 * d1 + d2 * d2 + e1 * e1 + e2 * e2);

    }

    skip_if(0); /* Catches NULL apertures and illegal index input */

    skip_if(pswapn == NULL);
    skip_if(appos == apneg);
    skip_if(ineg1 == ineg2);

    skip_if(ssize <= 0.0);

    *pswapn = swapn;

    result = ok/ssize; /* OK to divide now */

    end_skip;    

    return result;

}


/*----------------------------------------------------------------------------*/
/**
  @brief    Compute an eccentricity in pixels of a two-point object
  @param    apnear The apertures of the objects closest to (1,1)
  @param    inear  The index of the near object
  @param    apfar  The apertures of the objects farthest from (1,1)
  @param    ifar   The index of the far object
  @param    ssize  Expected distance from near to far object
  @param    is_hor True iff the alignment is to be horizontal, else vertical
  @return   The non-negative eccentricity, or a negative number on error
  @note     A negative number is returned when called with an existing error

  The eccentricity is zero when the points are in the specified
  (horizontal/vertical) direction with the given distance and
  increases as the two points define something less alike that.

 */
/*----------------------------------------------------------------------------*/
double visir_img_check_line(const cpl_apertures * apnear, int inear,
                            const cpl_apertures * apfar, int ifar,
                            double ssize, cpl_boolean is_hor)
{

    /* NB: Lower left pixel is (1, 1) */

    /* The near point */
    const double xn = cpl_apertures_get_centroid_x(apnear, inear);
    const double yn = cpl_apertures_get_centroid_y(apnear, inear);

    /* The far point */
    const double xf = cpl_apertures_get_centroid_x(apfar, ifar);
    const double yf = cpl_apertures_get_centroid_y(apfar, ifar);

    double result = -1.0; /* Assume failure */

    double ok;

    if (is_hor) {

        const double d = xf - xn - ssize; /* The length deviation */

        const double e = yf - yn;  /* The orthogonal deviation */

        ok = sqrt(d * d + e * e);

    } else {

        const double d = yf - yn - ssize; /* The length deviation */

        const double e = xf - xn;  /* The orthogonal deviation */

        ok = sqrt(d * d + e * e);

    }

    skip_if(0); /* Catches NULL apertures and illegal index input */

    skip_if(apnear == apfar);

    skip_if(ssize <= 0.0);

    result = ok/ssize; /* OK to divide now */

    end_skip;    

    return result;

}


/*----------------------------------------------------------------------------*/
/**
  @brief    Determine whether a file has CUBE2 format
  @param    rawframes   The rawframes
  @param    pos         The position of the frame
  @param    ptype       On success, set to VISIR_CUBE1 or VISIR_CUBE2
  @param    pnaxis3     On success, set to NAXIS3
  @param    pnchop      On success, set to NCHOP
  @return  CPL_ERROR_NONE or the relevant CPL error code on error

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code visir_get_type(const irplib_framelist * rawframes,
                                     int pos, visir_cube_type * ptype,
                                     int * pnaxis3, int * pnchop) {



    cpl_errorstate     cleanstate = cpl_errorstate_get();
    const char       * file
        = cpl_frame_get_filename(irplib_framelist_get_const(rawframes, pos));
    const cpl_propertylist * plist
        = irplib_framelist_get_propertylist_const(rawframes, pos);
    const int          nchop  = visir_pfits_get_chop_ncycles(plist);
    const int          naxis3 = visir_pfits_get_naxis3(plist);
    const char       * sval = NULL;
    /* If nchop == 1 (and naxis3 == 3) the data unit is both a valid CUBE1
       and a valid CUBE2; Assume the frame type to be CUBE2 */


    skip_if(0);
    bug_if(ptype == NULL);
    bug_if(pnaxis3 == NULL);

    sval = visir_pfits_get_frame_type(plist);
    if (sval == NULL) {
        /* Has warned about missing frame type card */
        visir_error_reset("Could not get FITS key");
        /* Don't know whether or not to expect CUBE1 or CUBE2 */
    } else if (strcmp(sval, "CUBE1")==0) {
        *ptype = VISIR_CUBE1;
    } else if (strcmp(sval, "CUBE2")==0) {
        *ptype = VISIR_CUBE2;
    }

    if (*ptype == VISIR_CUBE2) {
        if (naxis3 == 2 * nchop + 1) {
            /* OK. FRAME TYPE cannot be verified from naxis3 when nchop = 1 */
        } else if (naxis3 == nchop + 2) {
            cpl_msg_warning(cpl_func, "%s has FRAM TYPE = '%s', but NAXIS3=%d "
                            "and CHOP NCYCLES=%d imply a CUBE1. Assuming "
                            "the frame type is really CUBE1", file, sval,
                          naxis3, nchop);
            *ptype = VISIR_CUBE1;
        } else {
            cpl_msg_warning(cpl_func, "%s has FRAM TYPE = '%s', but NAXIS3=%d "
                            "and CHOP NCYCLES=%d is not a valid VISIR INTERM+"
                            "Half-Cycle format", file, sval, naxis3, nchop);
            skip_if(1);
        }
    } else if (*ptype == VISIR_CUBE1) {
        if (naxis3 == nchop + 2) {
            /* OK. FRAME TYPE cannot be verified from naxis3 when nchop = 1 */
            if (nchop > 1)
                cpl_msg_debug(cpl_func, "%s has %d INTERM-frames and one Half-"
                              "Cycle frame (old CUBE1-format)", file, nchop);

        } else if (naxis3 == 2 * nchop + 1) {
            cpl_msg_warning(cpl_func, "%s has FRAM TYPE = '%s', but NAXIS3=%d "
                            "and CHOP NCYCLES=%d imply a CUBE2. Assuming "
                            "the frame type is really CUBE2", file, sval,
                            naxis3, nchop);
            *ptype = VISIR_CUBE2;
        } else {
            cpl_msg_warning(cpl_func, "%s has FRAM TYPE = '%s', but NAXIS3=%d "
                            "and CHOP NCYCLES=%d is not a valid VISIR INTERM+"
                            "Half-Cycle format", file, sval, naxis3, nchop);
            skip_if(1);
        }
    } else if (naxis3 == 2 * nchop + 1) {
        cpl_msg_warning(cpl_func, "%s has FRAM TYPE='%s', but NAXIS3=%d and "
                        "CHOP NCYCLES=%d imply a CUBE2. Assuming the frame "
                        "type is CUBE2", file, sval ? sval : "<NULL>", naxis3,
                        nchop);
        *ptype = VISIR_CUBE2;
    } else if (naxis3 == nchop + 2) {
        cpl_msg_warning(cpl_func, "%s has FRAM TYPE='%s', but NAXIS3=%d and "
                        "CHOP NCYCLES=%d imply a CUBE1. Assuming the frame "
                        "type is CUBE1", file, sval ? sval : "<NULL>", naxis3,
                        nchop);
        *ptype = VISIR_CUBE1;
    } else {
       return cpl_error_set_message(cpl_func, CPL_ERROR_BAD_FILE_FORMAT,
                                    "%s has FRAM TYPE='%s', NAXIS3 = %d and "
                                    "CHOP NCYCLES = %d", file,
                                    sval ? sval : "<NULL>", naxis3, nchop);
    }

    *pnaxis3 = naxis3;
    *pnchop  = nchop;

    end_skip;

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
   @brief   find frame where chopping changes from on to off
   @param   self         The propertylist to analyze
   @param   ichopchange  Index of frame where chop happened counting from zero
   @param   ihalfcycle   Number of frames per halfcycle
   @return  CPL_ERROR_NONE iff OK.
*/
/*----------------------------------------------------------------------------*/
cpl_error_code visir_img_burst_find_delta_chop(const cpl_propertylist * self,
                                               int * ichopchange, int * ihalfcycle)
{

    const char * sdateobs   =
        cpl_propertylist_get_string(self, VISIR_PFITS_STRING_OBS_START);
    const char * schopstart =
        cpl_propertylist_get_string(self, VISIR_PFITS_STRING_CHOP_START);
    const double chop_freq  =
        cpl_propertylist_get_double(self, VISIR_PFITS_DOUBLE_CHOP_FREQ);
    const int nditskip  =
        cpl_propertylist_get_int(self, VISIR_PFITS_INT_NDITSKIP);
    const double dit        = visir_pfits_get_dit(self);
    double       ddateobs, dchopstart;
    double       period; /* Number of A+B frames in one full chopping cycle */

    skip_if(0);

    bug_if(irplib_wcs_mjd_from_string(&ddateobs, sdateobs));
    bug_if(irplib_wcs_mjd_from_string(&dchopstart, schopstart));

    skip_if(chop_freq <= 0.0);
    skip_if(dit <= 0.0);

    /* adapt for skipped exposures after obs start */
    ddateobs += dit * nditskip / (double)VISIR_SECS_PER_DAY;

    period = 1.0/(chop_freq * dit);

    /* FIXME: handle this case */
    bug_if((int)(period + 0.5) % 2 !=  0);

    *ihalfcycle = (int)(period + 0.5)/2;

    cpl_msg_info(cpl_func, "Number of A+B frames in one full chopping cycle: %g",
                 period);

    if (dchopstart < ddateobs) {
        double tchop = (ddateobs - dchopstart) * (double)VISIR_SECS_PER_DAY;
        /* Number of chopping cycles before obs+chopper start */
        double dprecycle = tchop * chop_freq;
        /* Phase to skip to point to first A frame
           in first complete chopping cycle */
        const double phase = ceil(dprecycle) - dprecycle;

        /* First valid frame is the frame at ddateobs (except for startindex) */
        *ichopchange = (int)ceil(phase * period) - 1;

        cpl_msg_info(cpl_func, "Chopping started %gs (%f cycles) before OBS start: "
                     "%f < %f", tchop, dprecycle, dchopstart, ddateobs);

    } else if (ddateobs < dchopstart) {
        /* FIXME: Allowed ? */
        /* First valid frame is the frame at dchopstart (except for startindex) */
        double tchop = (dchopstart - ddateobs) * (double)VISIR_SECS_PER_DAY;
        *ichopchange = (int)ceil(tchop / dit) - 1;
        cpl_msg_info(cpl_func, "Chopping started %gs (wasted %g cycles) after OBS "
                     "start: %f > %f", tchop, tchop * chop_freq, dchopstart,
                     ddateobs);
    } else {
        /* FIXME: Allowed ? */
        /* First valid frame is the first frame (at both ddateobs and dchopstart)
           (except for startindex) */
        *ichopchange = 0;
        cpl_msg_info(cpl_func, "Chopping started with OBS start: %f == %f",
                     dchopstart, ddateobs);
    }

    /* wrap value to cycle */
    *ichopchange = *ichopchange % (*ihalfcycle * 2);

    cpl_msg_info(cpl_func, "Frame of chop change: %d", *ichopchange);

    end_skip;

    return cpl_error_get_code();
}

#include "visir_destripe.c"
