/** @file pmSubtraction.c
 *
 *  @author Paul Price, IfA
 *  @author GLG, MHPCC
 *
 *  Copyright 2004-2007 Institute for Astronomy, University of Hawaii
 *
 */

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

#include <stdio.h>
#include <math.h>
#include <string.h>
#include <pslib.h>

#include "pmHDU.h"                      // Required for pmFPA.h
#include "pmFPA.h"
#include "pmSubtractionStamps.h"
#include "pmSubtractionEquation.h"
#include "pmSubtractionVisual.h"
#include "pmSubtractionThreads.h"

#include "pmSubtraction.h"

//#define TESTING

#define PIXEL_LIST_BUFFER 100           // Number of entries to add to pixel list at a time
#define MIN_SAMPLE_STATS    7           // Minimum number to use sample statistics; otherwise use quartiles
#define USE_KERNEL_ERR                  // Use kernel error image?

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Private (file-static) functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Generate the kernel to apply to the variance from the normal kernel
static psKernel *varianceKernel(psKernel *out, // Output kernel
                                const psKernel *normalKernel // Normal kernel
                                )
{
    // Kernel range
    int xMin = normalKernel->xMin, xMax = normalKernel->xMax;
    int yMin = normalKernel->yMin, yMax = normalKernel->yMax;

    if (!out) {
        out = psKernelAlloc(xMin, xMax, yMin, yMax);
    }

    // Take the square of the normal kernel
    double sumVariance = 0.0; // Sum of the variance kernels
    for (int v = yMin; v <= yMax; v++) {
        for (int u = xMin; u <= xMax; u++) {
            sumVariance += out->kernel[v][u] = PS_SQR(normalKernel->kernel[v][u]);
        }
    }

    // Normalise so that the sum of the variance kernel is the square of the sum of the normal kernel
    // This is required to keep the relative scaling between the image and the variance map
    psBinaryOp(out->image, out->image, "*", psScalarAlloc(1.0 / sumVariance, PS_TYPE_F32));

    return out;
}

// Contribute to an image of the solved kernel component using the preCalculated image
static void solvedKernelPreCalc(psKernel *kernel, // Kernel, updated
                                const pmSubtractionKernels *kernels, // Kernel basis functions
                                float value,                         // Normalisation value for basis function
                                int index                  // Index of basis function of interest
    )
{
    int size = kernels->size;           // Kernel half-size
    pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[index]; // Precalculated values
#if 0
    // Iterating over the kernel
    for (int y = 0, v = -size; v <= size; y++, v++) {
        float yValue = value * preCalc->yKernel->data.F32[y];
        for (int x = 0, u = -size; u <= size; x++, u++) {
            kernel->kernel[v][u] +=  yValue * preCalc->xKernel->data.F32[x];
        }
    }
    // Photometric scaling for even kernels only
    if (kernels->u->data.S32[i] % 2 == 0 && kernels->v->data.S32[i] % 2 == 0) {
        kernel->kernel[0][0] -= value;
    }
#else
    for (int v = -size; v <= size; v++) {
        for (int u = -size; u <= size; u++) {
            kernel->kernel[v][u] +=  value * preCalc->kernel->kernel[v][u];
        }
    }
#endif

    return;
}


// Generate an image of the solved kernel
static psKernel *solvedKernel(psKernel *kernel, // Kernel, to return
                              const pmSubtractionKernels *kernels, // Kernel basis functions
                              const psImage *polyValues, // Spatial polynomial values
                              bool normalise,            // Add normalisation?
                              bool wantDual // Want the dual (second) kernel?
                              )
{
    assert(kernels);
    assert(polyValues);

    int numKernels = kernels->num;      // Number of kernel basis functions
    int size = kernels->size;           // Kernel half-size
    if (!kernel) {
        kernel = psKernelAlloc(-size, size, -size, size);
    }
    psImageInit(kernel->image, 0.0);

    for (int i = 0; i < numKernels; i++) {
        double value = p_pmSubtractionSolutionCoeff(kernels, polyValues, i, wantDual); // Polynomial value
        if (wantDual) {
            // The model is built with the dual convolution terms added, so to produce zero residual the
            // equation results in negative coefficients which we must undo.
            value *= -1.0;
        }

        switch (kernels->type) {
          case PM_SUBTRACTION_KERNEL_POIS: {
              int u = kernels->u->data.S32[i]; // Offset in x
              int v = kernels->v->data.S32[i]; // Offset in y
              kernel->kernel[v][u] += value;
              kernel->kernel[0][0] -= value;
              break;
          }
          /* SPAM and FRIES use the same method */
          case PM_SUBTRACTION_KERNEL_SPAM:
          case PM_SUBTRACTION_KERNEL_FRIES: {
              int uStart = kernels->u->data.S32[i];
              int uStop = kernels->uStop->data.S32[i];
              int vStart = kernels->v->data.S32[i];
              int vStop = kernels->vStop->data.S32[i];

              // Normalising sum of kernel component to unity
              float norm = 1.0 / (float)((uStop - uStart + 1) * (vStop - vStart + 1));

              for (int v = vStart; v <= vStop; v++) {
                  for (int u = uStart; u <= uStop; u++) {
                      kernel->kernel[v][u] += norm * value;
                      kernel->kernel[0][0] -= value;
                  }
              }
              break;
          }
          case PM_SUBTRACTION_KERNEL_GUNK: {
              if (i < kernels->inner) {
                  solvedKernelPreCalc(kernel, kernels, value, i);
              } else {
                  // Using delta function
                  int u = kernels->u->data.S32[i]; // Offset in x
                  int v = kernels->v->data.S32[i]; // Offset in y
                  kernel->kernel[v][u] += value;
                  kernel->kernel[0][0] -= value;
              }
              break;
          }
          case PM_SUBTRACTION_KERNEL_ISIS:
          case PM_SUBTRACTION_KERNEL_ISIS_RADIAL:
          case PM_SUBTRACTION_KERNEL_HERM:
          case PM_SUBTRACTION_KERNEL_DECONV_HERM: {
              solvedKernelPreCalc(kernel, kernels, value, i);
              break;
          }
          case PM_SUBTRACTION_KERNEL_RINGS: {
              pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[i]; // Precalculated kernels
              int num = preCalc->uCoords->n;     // Number of pixels

              for (int j = 0; j < num; j++) {
                  int u = preCalc->uCoords->data.S32[j];
                  int v = preCalc->vCoords->data.S32[j]; // Kernel coordinates
                  kernel->kernel[v][u] += preCalc->poly->data.F32[j] * value;
              }
              // Photometric scaling is built into the kernel --- no subtraction!
              break;
          }
          default:
            psAbort("Should never get here.");
        }
    }

    if (normalise) {
        // Put in the normalisation component
        kernel->kernel[0][0] += (wantDual ? 1.0 : p_pmSubtractionSolutionNorm(kernels));
    }

    return kernel;
}

// Subtract the (0,0) element to preserve photometric scaling
static void convolveSub(psKernel *convolved, // Convolved image
                        const psKernel *image, // Image being convolved
                        int footprint  // Size of region of interest
                        )
{
    // Can't use psBinaryOp because the images are of different size
    for (int y = -footprint; y <= footprint; y++) {
        for (int x = -footprint; x <= footprint; x++) {
            convolved->kernel[y][x] -= image->kernel[y][x];
        }
    }
    return;
}

// Generate the convolution given some offset
static psKernel *convolveOffset(const psKernel *image, // Image to convolve (a kernel for convenience)
                                int u, int v, // Offset to apply
                                int footprint // Size of region of interest
                                )
{
    psKernel *convolved = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Convolved image
    for (int y = -footprint; y <= footprint; y++) {
        for (int x = -footprint; x <= footprint; x++) {
            convolved->kernel[y][x] = image->kernel[y - v][x - u];
        }
    }
    return convolved;
}


// Convolve an image using FFT
static void convolveFFT(psImage *target,// Place the result in here
                        psImage *image, // Image to convolve
                        psImage *mask, // Mask image
                        psImageMaskType maskVal, // Value to mask
                        const psKernel *kernel, // Kernel by which to convolve
                        psRegion region,// Region of interest
                        float background, // Background to add
                        int size        // Size of (square) kernel
                        )
{
    psRegion border = psRegionSet(region.x0 - size, region.x1 + size,
                                  region.y0 - size, region.y1 + size); // Add a border

    psImage *subImage = image ? psImageSubset(image, border) : NULL; // Subimage to convolve
    psImage *subMask = mask ? psImageSubset(mask, border) : NULL; // Subimage mask

    psImage *convolved = psImageConvolveFFT(NULL, subImage, subMask, maskVal, kernel); // Convolution

    psFree(subImage);
    psFree(subMask);

    // Now, we have to stick it in where it belongs
    int xMin = region.x0, xMax = region.x1, yMin = region.y0, yMax = region.y1; // Bounds of region
    if (background != 0.0) {
        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
            for (int xTarget = xMin, xSource = size; xTarget < xMax; xTarget++, xSource++) {
                target->data.F32[yTarget][xTarget] = convolved->data.F32[ySource][xSource] + background;
            }
        }
    } else {
        int numBytes = (xMax - xMin) * PSELEMTYPE_SIZEOF(PS_TYPE_F32); // Number of bytes to copy
        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
            memcpy(&target->data.F32[yTarget][xMin], &convolved->data.F32[ySource][size], numBytes);
        }
    }
    psFree(convolved);

    return;
}


// Convolve an image using FFT
static void convolveVarianceFFT(psImage *target,// Place the result in here
                              psImage *variance, // Variance map to convolve
                              psImage *kernelErr, // Kernel error image
                              psImage *mask, // Mask image
                              psImageMaskType maskVal, // Value to mask
                              const psKernel *kernel, // Kernel by which to convolve
                              psRegion region,// Region of interest
                              int size        // Size of (square) kernel
                              )
{
    psRegion border = psRegionSet(region.x0 - size, region.x1 + size,
                                  region.y0 - size, region.y1 + size); // Add a border

    psImage *subVariance = variance ? psImageSubset(variance, border) : NULL; // Variance map
    psImage *subKE = kernelErr ? psImageSubset(kernelErr, border) : NULL; // Kernel error image
    psImage *subMask = mask ? psImageSubset(mask, border) : NULL; // Mask

    // XXX Can trim this a little by combining the convolution: only have to take the FFT of the kernel once
    psImage *convVariance = psImageConvolveFFT(NULL, subVariance, subMask, maskVal, kernel); // Convolved variance
    psImage *convKE = subKE ? psImageConvolveFFT(NULL, subKE, subMask, maskVal, kernel) : NULL; // Conv KE

    psFree(subVariance);
    psFree(subKE);
    psFree(subMask);

    // Now, we have to stick it in where it belongs
    int xMin = region.x0, xMax = region.x1, yMin = region.y0, yMax = region.y1; // Bounds of region
    if (convKE) {
        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
            for (int xTarget = xMin, xSource = size; xTarget < xMax; xTarget++, xSource++) {
                target->data.F32[yTarget][xTarget] = convVariance->data.F32[ySource][xSource] +
                    convKE->data.F32[ySource][xSource];
            }
        }
    } else {
        int numBytes = (xMax - xMin) * PSELEMTYPE_SIZEOF(PS_TYPE_F32); // Number of bytes to copy
        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
            memcpy(&target->data.F32[yTarget][xMin], &convVariance->data.F32[ySource][size], numBytes);
        }
    }

    psFree(convVariance);
    psFree(convKE);

    return;
}


// Convolve an image directly
static void convolveDirect(psImage *target, // Put the result here
                           const psImage *image, // Image to convolve
                           const psKernel *kernel, // Kernel by which to convolve
                           psRegion region,// Region of interest
                           float background, // Background to add
                           int size        // Size of (square) kernel
                           )
{
    for (int y = region.y0; y < region.y1; y++) {
        for (int x = region.x0; x < region.x1; x++) {
            target->data.F32[y][x] = background;
            for (int v = -size; v <= size; v++) {
                for (int u = -size; u <= size; u++) {
                    target->data.F32[y][x] += kernel->kernel[v][u] * image->data.F32[y - v][x - u];
                }
            }
        }
    }
    return;
}

// Convolve a region of an image
static inline void convolveRegion(psImage *convImage, // Convolved image (output)
                                  psImage *convVariance, // Convolved variance map (output), or NULL
                                  psImage *convMask, // Convolve mask (output), or NULL
                                  psKernel **kernelImage, // Convolution kernel for the image
                                  psKernel **kernelVariance, // Convolution kernel for the variance map, or NULL
                                  psImage *image, // Image to convolve
                                  psImage *variance, // Variance map to convolve, or NULL
                                  psImage *kernelErr, // Kernel error image, or NULL
                                  psImage *subMask, // Subtraction mask
                                  const pmSubtractionKernels *kernels, // Kernels
                                  const psImage *polyValues, // Polynomial values
                                  float background, // Background value to apply
                                  psRegion region, // Region to convolve
                                  psImageMaskType maskBad, // Value to give bad pixels
                                  psImageMaskType maskPoor, // Value to give poor pixels
                                  float poorFrac, // Fraction for "poor"
                                  bool useFFT,  // Use FFT to convolve?
                                  bool wantDual // Want the dual convolution?
    )
{
    *kernelImage = solvedKernel(*kernelImage, kernels, polyValues, true, wantDual);
    if (variance || subMask) {
        *kernelVariance = varianceKernel(*kernelVariance, *kernelImage);
    }

    psImageMaskType subBad;                  // Bad pixels in subtraction mask
    psImageMaskType subConvBad;              // Bad pixels in subtraction mask when convolving
    psImageMaskType subConvPoor;             // Poor pixels in subtraction mask when convolving
    if (kernels->mode == PM_SUBTRACTION_MODE_1 || (kernels->mode == PM_SUBTRACTION_MODE_DUAL && !wantDual)) {
        subBad = PM_SUBTRACTION_MASK_BAD_1;
        subConvBad = PM_SUBTRACTION_MASK_CONVOLVE_BAD_1;
        subConvPoor = PM_SUBTRACTION_MASK_CONVOLVE_1;
    } else {
        subBad = PM_SUBTRACTION_MASK_BAD_2;
        subConvBad = PM_SUBTRACTION_MASK_CONVOLVE_BAD_2;
        subConvPoor = PM_SUBTRACTION_MASK_CONVOLVE_2;
    }

    // Convolve the image and variance
    if (useFFT) {
        // Use Fast Fourier Transform to do the convolution
        // This provides a big speed-up for large kernels
        convolveFFT(convImage, image, subMask, subBad, *kernelImage, region, background, kernels->size);
        if (variance) {
            convolveVarianceFFT(convVariance, variance, kernelErr, subMask, subBad, *kernelVariance,
                                region, kernels->size);
        }
    } else {
        // XXX Direct convolution doesn't account for bad pixels yet
        convolveDirect(convImage, image, *kernelImage, region, background, kernels->size);
        if (variance) {
            convolveDirect(convVariance, variance, *kernelVariance, region, 0.0, kernels->size);
        }
    }

    // Convolve the mask for bad/poor pixels
    if (subMask && convMask) {
        int box = p_pmSubtractionBadRadius(*kernelImage, kernels, polyValues,
                                           wantDual, poorFrac); // Size of bad box
        psAssert(box >= 0, "Bad radius must be >= 0");

        int colMin = region.x0, colMax = region.x1, rowMin = region.y0, rowMax = region.y1; // Bounds
        psImage *convolved = NULL; // Convolved subtraction mask
        if (box > 0) {
            psRegion maskRegion = psRegionSet(colMin - box, colMax + box,
                                              rowMin - box, rowMax + box); // Region to convolve
            psImage *image = subMask ? psImageSubset(subMask, maskRegion) : NULL; // Mask to convolve
            convolved = psImageConvolveMask(NULL, image, subBad, subConvBad, -box, box, -box, box);
            psFree(image);
        } else {
            convolved = psImageSubset(subMask, region);
        }

        psAssert(convolved->numCols - 2 * box == colMax - colMin, "Bad number of columns");
        psAssert(convolved->numRows - 2 * box == rowMax - rowMin, "Bad number of rows");

        for (int yTarget = rowMin, ySource = box; yTarget < rowMax; yTarget++, ySource++) {
            // Dereference images
            psImageMaskType *target = &convMask->data.PS_TYPE_IMAGE_MASK_DATA[yTarget][colMin]; // Target values
            psImageMaskType *source = &convolved->data.PS_TYPE_IMAGE_MASK_DATA[ySource][box]; // Source values
            for (int xTarget = colMin; xTarget < colMax; xTarget++, target++, source++) {
                if (*source & subConvBad) {
                    *target |= maskBad;
                } else if (*source & subConvPoor) {
                    *target |= maskPoor;
                }
            }
        }

        psFree(convolved);
    }

    return;
}

#ifdef USE_KERNEL_ERR
// Generate an image that can be used to track systematic errors in the kernel
static psImage *subtractionKernelErrImage(const psImage *image, // Image from which to make kernel error image
                                          float kernelError // Relative systematic error in kernel
    )
{
    if (!isfinite(kernelError) || kernelError == 0.0) {
        return NULL;
    }

    int numCols = image->numCols, numRows = image->numRows; // Size of image
    psImage *kernelErr = psImageAlloc(numCols, numRows, PS_TYPE_F32); // Kernel error image

    float kernelError2 = PS_SQR(kernelError); // Square of the kernel error
    for (int y = 0; y < numRows; y++) {
        for (int x = 0; x < numCols; x++) {
            kernelErr->data.F32[y][x] = PS_SQR(image->data.F32[y][x]) * kernelError2;
        }
    }

    return kernelErr;
}
#endif

// Convolve a stamp using a pre-calculated kernel basis function
static psKernel *convolveStampPreCalc(const psKernel *image, // Image to convolve
                                      const pmSubtractionKernels *kernels, // Kernel basis functions
                                      int index,                            // Index of basis function of interest
                                      int footprint                         // Half-size of stamp
    )
{
    pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[index]; // Precalculated data
#if 0
    // Convolving using separable convolution
    int size = kernels->size;     // Size of kernel

    // Convolve in x
    // Need to convolve a bit more than the footprint, for the y convolution
    int yMin = -size - footprint, yMax = size + footprint; // Range for y
    psKernel *temp = psKernelAlloc(yMin, yMax,
                                   -footprint, footprint); // Temporary convolution; NOTE: wrong way!
    for (int y = yMin; y <= yMax; y++) {
        for (int x = -footprint; x <= footprint; x++) {
            float value = 0.0;    // Value of convolved pixel
            int uMin = x - size, uMax = x + size; // Range for u
            psF32 *xKernelData = &preCalc->xKernel->data.F32[xKernel->n - 1]; // Kernel values
            psF32 *imageData = &image->kernel[y][uMin]; // Image values
            for (int u = uMin; u <= uMax; u++, xKernelData--, imageData++) {
                value += *xKernelData * *imageData;
            }
            temp->kernel[x][y] = value; // NOTE: putting in wrong way!
        }
    }

    // Convolve in y
    psKernel *convolved = psKernelAlloc(-footprint, footprint, -footprint, footprint);// Convolved image
    for (int x = -footprint; x <= footprint; x++) {
        for (int y = -footprint; y <= footprint; y++) {
            float value = 0.0;    // Value of convolved pixel
            int vMin = y - size, vMax = y + size; // Range for v
            psF32 *yKernelData = &preCalc->yKernel->data.F32[yKernel->n - 1]; // Kernel values
            psF32 *imageData = &temp->kernel[x][vMin]; // Image values; NOTE: wrong way!
            for (int v = vMin; v <= vMax; v++, yKernelData--, imageData++) {
                value += *yKernelData * *imageData;
            }
            convolved->kernel[y][x] = value;
        }
    }
    psFree(temp);

    // Photometric scaling for even kernels only
    if (kernels->u->data.S32[index] % 2 == 0 && kernels->v->data.S32[index] % 2 == 0) {
        convolveSub(convolved, image, footprint);
    }
    return convolved;
#else
    // Convolving using precalculated kernel
    return p_pmSubtractionConvolveStampPrecalc(image, preCalc->kernel);
#endif
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Semi-public functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

psKernel *p_pmSubtractionConvolveStampPrecalc(const psKernel *image, const psKernel *kernel)
{
    PS_ASSERT_KERNEL_NON_NULL(image, NULL);
    PS_ASSERT_KERNEL_NON_NULL(kernel, NULL);

    psImage *conv = psImageConvolveFFT(NULL, image->image, NULL, 0, kernel); // Convolved image
    int x0 = - image->xMin, y0 = - image->yMin; // Position of centre of convolved image
    psKernel *convolved = psKernelAllocFromImage(conv, x0, y0); // Kernel version

    // pmSubtractionVisualShowSubtraction(image->image, kernel->image, conv);

    psFree(conv);
    return convolved;
}

int p_pmSubtractionBadRadius(psKernel *preKernel, const pmSubtractionKernels *kernels,
                             const psImage *polyValues, bool wantDual, float poorFrac)
{
    psKernel *kernel;                   // Kernel to use
    if (!preKernel) {
        kernel = solvedKernel(NULL, kernels, polyValues, true, wantDual);
    } else {
        kernel = psMemIncrRefCounter(preKernel);
    }
    PS_ASSERT_IMAGE_NON_NULL(polyValues, -1);

    int xMin = kernel->xMin, xMax = kernel->xMax, yMin = kernel->yMin, yMax = kernel->yMax; // Bounds

    // Determine the threshold between bad and poor
    double sumKernel2 = 0.0;            // Sum of the kernel-squared
    for (int y = yMin; y <= yMax; y++) {
        for (int x = xMin; x <= xMax; x++) {
            sumKernel2 += PS_SQR(kernel->kernel[y][x]);
        }
    }
    float threshold = sumKernel2 * poorFrac; // Threshold between poor and bad

    // Get bounds of threshold region
    // Start with the entire kernel, and keep reducing the size of the box until it sum goes above threshold
    int box = kernels->size;            // Size of box with bad pixels
    for (double sumBox = 0.0; sumBox < threshold && box > 0; box--) {
        for (int x = -box; x <= box; x++) {
            sumBox += PS_SQR(kernel->kernel[-box][x]) + PS_SQR(kernel->kernel[box][x]);
        }
        for (int y = -box + 1; y <= box - 1; y++) {
            // Note: not doing corners
            sumBox += PS_SQR(kernel->kernel[y][-box]) + PS_SQR(kernel->kernel[y][box]);
        }
    }

    psFree(kernel);

    return box;
}

psImage *p_pmSubtractionPolynomialFromCoords(psImage *output, const pmSubtractionKernels *kernels,
                                             int numCols, int numRows, int x, int y)
{
    assert(kernels);
    assert(numCols > 0 && numRows > 0);

    // Size to use when calculating normalised coordinates (different from actual size when convolving
    // subimage)
    int xNormSize = (kernels->numCols > 0 ? kernels->numCols : numCols);
    int yNormSize = (kernels->numRows > 0 ? kernels->numRows : numRows);

    // Normalised coordinates
    float yNorm = 2.0 * (float)(y - yNormSize/2.0) / (float)yNormSize;
    float xNorm = 2.0 * (float)(x - xNormSize/2.0) / (float)xNormSize;

    return p_pmSubtractionPolynomial(output, kernels->spatialOrder, xNorm, yNorm);
}

psImage *p_pmSubtractionPolynomial(psImage *output, int spatialOrder, float x, float y)
{
    assert(spatialOrder >= 0);
    assert(x >= -1 && x <= 1);
    assert(y >= -1 && y <= 1);

    output = psImageRecycle(output, spatialOrder + 1, spatialOrder + 1, PS_TYPE_F64);
    output->data.F64[0][0] = 1.0;

    double value = 1.0;
    for (int i = 1; i <= spatialOrder; i++) {
        value *= x;
        output->data.F64[0][i] = value;
    }

    value = 1.0;
    for (int j = 1; j <= spatialOrder; j++) {
        value *= y;
        output->data.F64[j][0] = value;
    }

    for (int j = 1; j <= spatialOrder; j++) {
        for (int i = 1; i <= spatialOrder - j; i++) {
            output->data.F64[j][i] = output->data.F64[j][0] * output->data.F64[0][i];
        }
    }

    return output;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Public functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////


// Convolve a stamp by a single kernel basis function
static psKernel *convolveStampSingle(const pmSubtractionKernels *kernels, // Kernel basis functions
                                     int index, // Kernel basis function index
                                     const psKernel *image, // Image to convolve (a kernel for convenience)
                                     int footprint // Size of region of interest
    )
{
    switch (kernels->type) {
      case PM_SUBTRACTION_KERNEL_POIS: {
          int u = kernels->u->data.S32[index]; // Offset in x
          int v = kernels->v->data.S32[index]; // Offset in y
          psKernel *convolved = convolveOffset(image, u, v, footprint); // Convolved image
          convolveSub(convolved, image, footprint);
          return convolved;
      }
        // Method for SPAM and FRIES is the same
      case PM_SUBTRACTION_KERNEL_SPAM:
      case PM_SUBTRACTION_KERNEL_FRIES: {
          psKernel *convolved = psKernelAlloc(-footprint, footprint,
                                              -footprint, footprint); // Convolved image
          int uStart = kernels->u->data.S32[index];
          int uStop = kernels->uStop->data.S32[index];
          int vStart = kernels->v->data.S32[index];
          int vStop = kernels->vStop->data.S32[index];
          float norm = 1.0 / (uStop - uStart + 1) * (vStop - vStart + 1); // Normalisation
          for (int y = -footprint; y <= footprint; y++) {
              for (int x = -footprint; x <= footprint; x++) {
                  double sum = 0.0;
                  for (int v = vStart; v <= vStop; v++) {
                      for (int u = uStart; u <= uStop; u++) {
                          sum += image->kernel[y - v][x - u];
                      }
                  }
                  convolved->kernel[y][x] = norm * sum;
              }
          }
          convolveSub(convolved, image, footprint);
          return convolved;
      }
      case PM_SUBTRACTION_KERNEL_GUNK: {
          if (index < kernels->inner) {
              // Photometric scaling is already built in to the precalculated kernel
              return convolveStampPreCalc(image, kernels, index, footprint);
          }
          // Using delta function
          int u = kernels->u->data.S32[index]; // Offset in x
          int v = kernels->v->data.S32[index]; // Offset in y
          psKernel *convolved = convolveOffset(image, u, v, footprint); // Convolved image
          convolveSub(convolved, image, footprint);
          return convolved;
      }
      case PM_SUBTRACTION_KERNEL_ISIS:
      case PM_SUBTRACTION_KERNEL_ISIS_RADIAL:
      case PM_SUBTRACTION_KERNEL_HERM:
      case PM_SUBTRACTION_KERNEL_DECONV_HERM: {
            return convolveStampPreCalc(image, kernels, index, footprint);
        }
      case PM_SUBTRACTION_KERNEL_RINGS: {
          psKernel *convolved = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Convolved image
          pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[index]; // Precalculated data

          int num = preCalc->uCoords->n;         // Number of pixels
          psS32 *uData = preCalc->uCoords->data.S32; // Dereference v coordinate
          psS32 *vData = preCalc->vCoords->data.S32; // Dereference u coordinate
          psF32 *polyData = preCalc->poly->data.F32; // Dereference polynomial values
          psF32 **imageData = image->kernel;  // Dereference image
          psF32 **convData = convolved->kernel; // Dereference convolved image
          for (int y = -footprint; y <= footprint; y++) {
              for (int x = -footprint; x <= footprint; x++) {
                  double sum = 0.0;             // Accumulated sum from convolution
                  for (int j = 0; j < num; j++) {
                      int u = uData[j], v = vData[j]; // Kernel coordinates
                      sum += imageData[y - v][x - u] * polyData[j];
                  }
                  convData[y][x] = sum;
                  // Photometric scaling is built into the kernel --- no subtraction!
              }
          }
          return convolved;
      }
      default:
        psAbort("Should never get here.");
    }
    return NULL;
}

// Convolve the stamp by each of the kernel basis functions
static psArray *convolveStamp(psArray *convolutions, // The convolutions
                              const psKernel *image, // Image to convolve
                              const pmSubtractionKernels *kernels, // Kernel basis functions
                              int footprint // Stamp half-size
    )
{
    assert(image);
    assert(kernels);
    assert(footprint >= 0);

    if (convolutions) {
        // Already done
        return convolutions;
    }

    int numKernels = kernels->num;      // Number of kernels
    convolutions = psArrayAlloc(numKernels);

    for (int i = 0; i < numKernels; i++) {
        convolutions->data[i] = convolveStampSingle(kernels, i, image, footprint);
    }

    return convolutions;
}


bool pmSubtractionConvolveStamp(pmSubtractionStamp *stamp, const pmSubtractionKernels *kernels, int footprint)
{
    PS_ASSERT_PTR_NON_NULL(stamp, false);
    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
    PS_ASSERT_INT_NONNEGATIVE(footprint, false);

    if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Stamp not marked for calculation.");
        return false;
    }

    switch (kernels->mode) {
      case PM_SUBTRACTION_MODE_1:
        stamp->convolutions1 = convolveStamp(stamp->convolutions1, stamp->image1, kernels, footprint);
        break;
      case PM_SUBTRACTION_MODE_2:
        stamp->convolutions2 = convolveStamp(stamp->convolutions2, stamp->image2, kernels, footprint);
        break;
      case PM_SUBTRACTION_MODE_UNSURE:
      case PM_SUBTRACTION_MODE_DUAL:
        stamp->convolutions1 = convolveStamp(stamp->convolutions1, stamp->image1, kernels, footprint);
        stamp->convolutions2 = convolveStamp(stamp->convolutions2, stamp->image2, kernels, footprint);
        break;
      default:
        psAbort("Unsupported subtraction mode: %x", kernels->mode);
    }

    return true;
}


int pmSubtractionRejectStamps(pmSubtractionKernels *kernels, pmSubtractionStampList *stamps,
                              const psVector *deviations, psImage *subMask, float sigmaRej)
{
    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, -1);
    PS_ASSERT_VECTOR_NON_NULL(deviations, -1);
    PS_ASSERT_VECTOR_TYPE(deviations, PS_TYPE_F32, -1);
    PS_ASSERT_IMAGE_NON_EMPTY(subMask, -1);
    PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_IMAGE_MASK, -1);

    // I used to measure the rms deviation about zero, and use that as the sigma against which to clip, but
    // the distribution is actually something like a chi^2 or Student's t, both of which become Gaussian-like
    // with large N.  Therefore, let's just treat this as a Gaussian distribution.

    kernels->mean = NAN;
    kernels->rms = NAN;
    kernels->numStamps = -1;

    int numStamps = 0;                  // Number of used stamps
    psVector *mask = psVectorAlloc(stamps->num, PS_TYPE_VECTOR_MASK); // Mask, for statistics
    psVectorInit(mask, 0);
    for (int i = 0; i < stamps->num; i++) {
        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
        if (stamp->status != PM_SUBTRACTION_STAMP_USED) {
            mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0xff;
            continue;
        }
        numStamps++;
    }
    psTrace("psModules.imcombine", 1, "Number of good stamps: %d\n", numStamps);

    if (numStamps == 0) {
        psError(PS_ERR_UNKNOWN, true, "No good stamps found.");
        psFree(mask);
        return -1;
    }

    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV |
                                  PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_QUARTILE); // Statistics for deviatns
    if (!psVectorStats(stats, deviations, NULL, mask, 0xff)) {
        psError(PS_ERR_UNKNOWN, false, "Unable to measure statistics for deviations.");
        psFree(stats);
        psFree(mask);
        return -1;
    }
    psFree(mask);

    // XXX raise an error?
    if (isnan(stats->sampleMean)) {
        psFree(stats);
        return -1;
    }

    double mean, rms;                 // Mean and RMS of deviations
    if (numStamps < MIN_SAMPLE_STATS) {
        mean = stats->sampleMean;
        rms = stats->sampleStdev;
    } else {
        mean = stats->sampleMedian;
        rms = 0.74 * (stats->sampleUQ - stats->sampleLQ);
    }
    psFree(stats);

    psTrace("psModules.imcombine", 1, "Mean: %f\n", mean);
    psTrace("psModules.imcombine", 1, "RMS deviation: %f\n", rms);

    kernels->mean = mean;
    kernels->rms = rms;
    kernels->numStamps = numStamps;

    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Mean deviation from %d stamps: %lf +/- %lf",
             numStamps, mean, rms);

    if (!isfinite(sigmaRej) || sigmaRej <= 0.0) {
        // User just wanted to calculate and record the deviation for posterity
        return 0;
    }

    float limit = sigmaRej * rms; // Limit on maximum deviation
    psTrace("psModules.imcombine", 1, "Deviation limit: %f\n", limit);


    psString ds9name = NULL;            // Filename for ds9 region file
    static int ds9num = 0;              // File number for ds9 region file
    psStringAppend(&ds9name, "stamps_reject_%d.ds9", ds9num);
    FILE *ds9 = pmSubtractionStampsFile(stamps, ds9name, "rejected stamps");
    psFree(ds9name);
    ds9num++;

    int footprint = stamps->footprint;  // Half-size of stamp region of interest
    int numRejected = 0;                // Number of stamps rejected
    int numGood = 0;                    // Number of good stamps
    double newMean = 0.0;               // New mean
    psString log = NULL;                // Log message
    psStringAppend(&log, "Rejecting stamps, mean = %f, threshold = %f\n", mean, limit);
    for (int i = 0; i < stamps->num; i++) {
        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
        if (stamp->status == PM_SUBTRACTION_STAMP_USED) {
            // Should we reject stars with low deviation?  Well, if this is really a Gaussian-like
            // distribution and they're low, then we have the right to ask why.  Isn't it suspicious that
            // they're anomalously low, compared to the rest of the population which (we hope) is indicative
            // of normality?  Besides, the standard deviation is going to be blown up by stars that didn't
            // subtract well, in which case very few (if any) stars will be legitimately rejected for being
            // low.
            if (fabsf(deviations->data.F32[i] - mean) > limit) {
                // Mask out the stamp in the image so you it's not found again
                psTrace("psModules.imcombine", 3, "Rejecting stamp %d (%d,%d)\n", i,
                        (int)(stamp->x + 0.5), (int)(stamp->y + 0.5));
                psStringAppend(&log, "Stamp %d (%d,%d): %f\n", i,
                               (int)(stamp->x + 0.5), (int)(stamp->y + 0.5),
                               fabsf(deviations->data.F32[i] - mean));
                numRejected++;
                for (int y = stamp->y - footprint; y <= stamp->y + footprint; y++) {
                    for (int x = stamp->x - footprint; x <= stamp->x + footprint; x++) {
                        subMask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= PM_SUBTRACTION_MASK_REJ;
                    }
                }
                pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "red");

                // Set stamp for replacement
                stamp->x = 0;
                stamp->y = 0;
                stamp->xNorm = NAN;
                stamp->yNorm = NAN;
                stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
                // Recalculate convolutions
                psFree(stamp->convolutions1);
                psFree(stamp->convolutions2);
                stamp->convolutions1 = stamp->convolutions2 = NULL;
                psFree(stamp->image1);
                psFree(stamp->image2);
                psFree(stamp->weight);
                stamp->image1 = stamp->image2 = stamp->weight = NULL;
                psFree(stamp->matrix);
                stamp->matrix = NULL;
                psFree(stamp->vector);
                stamp->vector = NULL;
            } else {
                numGood++;
                newMean += deviations->data.F32[i];
                pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "green");
            }
        }
    }
    newMean /= numGood;

    if (numRejected == 0) {
        psStringAppend(&log, "<none>\n");
    }
    psLogMsg("psModules.imcombine", PS_LOG_DETAIL, "%s", log);
    psFree(log);

    if (ds9) {
        fclose(ds9);
    }

    if (numRejected > 0) {
        psLogMsg("psModules.imcombine", PS_LOG_INFO,
                 "%d good stamps; %d rejected.\nMean deviation: %lf --> %lf\n",
                 numGood, numRejected, mean, newMean);
    } else {
        psLogMsg("psModules.imcombine", PS_LOG_INFO,
                 "%d good stamps; 0 rejected.\nMean deviation: %lf\n",
                 numGood, mean);
    }

    return numRejected;
}

psKernel *pmSubtractionKernel(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
{
    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);

    psImage *polyValues = p_pmSubtractionPolynomial(NULL, kernels->spatialOrder, x, y); // Solved polynomial
    psKernel *kernel = solvedKernel(NULL, kernels, polyValues, true, wantDual); // The appropriate kernel
    psFree(polyValues);

    return kernel;
}

psImage *pmSubtractionKernelImage(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
{
    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);

    psKernel *kernel = pmSubtractionKernel(kernels, x, y, wantDual); // Convolution kernel
    psImage *image = psMemIncrRefCounter(kernel->image); // Image of the kernel
    psFree(kernel);

    return image;
}


float pmSubtractionVarianceFactor(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
{
    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NAN);
    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NAN);
    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NAN);
    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NAN);

    // Precalulate polynomial values
    psImage *polyValues = p_pmSubtractionPolynomial(NULL, kernels->spatialOrder, x, y);

    psKernel *kernel = solvedKernel(NULL, kernels, polyValues, true, wantDual); // The appropriate kernel
    psFree(polyValues);

    double sumKernel2 = 0.0;            // Sum of the kernel squared
    double sumKernel = 0.0;             // Sum of the kernel
    for (int y = kernel->yMin; y <= kernel->yMax; y++) {
        for (int x = kernel->xMin; x <= kernel->xMax; x++) {
            sumKernel += kernel->kernel[y][x];
            sumKernel2 += PS_SQR(kernel->kernel[y][x]);
        }
    }

    psFree(kernel);

    return sumKernel2 / PS_SQR(sumKernel);
}

#if 1
psArray *pmSubtractionKernelSolutions(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
{
    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);

    psVector *solution = wantDual ? kernels->solution2 : kernels->solution1; // Solution of interest
    psVector *backup = psVectorCopy(NULL, solution, PS_TYPE_F64);  // Backup version

    int num = kernels->num;             // Number of kernel basis functions

    psImage *polyValues = p_pmSubtractionPolynomial(NULL, kernels->spatialOrder, x, y); // Solved polynomial
    psArray *images = psArrayAlloc(num + 1); // Images of each kernel to return

    // The whole kernel
    {
        psKernel *kernel = solvedKernel(NULL, kernels, polyValues, true, wantDual); // The appropriate kernel
        images->data[0] = psMemIncrRefCounter(kernel->image);
        psFree(kernel);
    }

    // The parts
    psVectorInit(solution, 0.0);
    for (int i = 0; i < num; i++) {
        solution->data.F64[i] = backup->data.F64[i];
        psKernel *kernel = solvedKernel(NULL, kernels, polyValues, false, wantDual); // The appropriate kernel
#if 0
        int size = kernels->size;
        double sum = 0.0;
        for (int v = -size; v <= size; v++) {
            for (int u = -size; u <= size; u++) {
                sum += kernel->kernel[v][u];
            }
        }
        fprintf(stderr, "Kernel %d: %lf\n", i, sum);
#endif
        images->data[i + 1] = psMemIncrRefCounter(kernel->image);
        psFree(kernel);
        solution->data.F64[i] = 0.0;
    }
    psFree(polyValues);
    psVectorCopy(solution, backup, PS_TYPE_F64);
    psFree(backup);

    return images;
}
#endif


// XXX Put kernelImage, kernelVariance and polyValues on thread-dependent data
static bool subtractionConvolvePatch(int numCols, int numRows, // Size of image
                                     int x0, int y0, // Offsets for image
                                     pmReadout *out1, pmReadout *out2, // Output readouts
                                     psImage *convMask, // Output convolved mask
                                     const pmReadout *ro1, const pmReadout *ro2, // Input readouts
                                     psImage *kernelErr1, psImage *kernelErr2, // Kernel error images
                                     psImage *subMask, // Input subtraction mask
                                     psImageMaskType maskBad, // Mask value to give bad pixels
                                     psImageMaskType maskPoor, // Mask value to give poor pixels
                                     float poorFrac, // Fraction for "poor"
                                     const psRegion *region, // Patch to convolve
                                     const pmSubtractionKernels *kernels, // Kernels
                                     bool doBG, // Add in background when convolving?
                                     bool useFFT // Use FFT to do the convolution?
    )
{
    int size = kernels->size;           // Half-size of kernel
    int xMin = region->x0, xMax = region->x1, yMin = region->y0, yMax = region->y1; // Bounds of patch

    psKernel *kernelImage = NULL;       // Kernel for the images
    psKernel *kernelVariance = NULL;      // Kernel for the variance maps

    // Only generate polynomial values every kernel footprint, since we have already assumed
    // (with the stamps) that it does not vary rapidly on this scale.
    psImage *polyValues = p_pmSubtractionPolynomialFromCoords(NULL, kernels, numCols, numRows,
                                                              xMin + x0 + size + 1,
                                                              yMin + y0 + size + 1);
    float background = doBG ? p_pmSubtractionSolutionBackground(kernels, polyValues) : 0.0; // Background term

    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        convolveRegion(out1->image, out1->variance, out1->mask, &kernelImage, &kernelVariance,
                       ro1->image, ro1->variance, kernelErr1, subMask, kernels, polyValues, background,
                       *region, maskBad, maskPoor, poorFrac, useFFT, false);
    }
    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        convolveRegion(out2->image, out2->variance, out2->mask, &kernelImage, &kernelVariance,
                       ro2->image, ro2->variance, kernelErr2, subMask, kernels, polyValues, background,
                       *region, maskBad, maskPoor, poorFrac, useFFT,
                       kernels->mode == PM_SUBTRACTION_MODE_DUAL);
    }

    psFree(kernelImage);
    psFree(kernelVariance);
    psFree(polyValues);

    if ((kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) && ro1->mask) {
        psImageMaskType **target = out1->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Target mask
        psImageMaskType **source = ro1->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Source mask

        for (int y = yMin; y < yMax; y++) {
            for (int x = xMin; x < xMax; x++) {
                target[y][x] |= source[y][x];
            }
        }
    }
    if ((kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) && ro2->mask) {
        psImageMaskType **target = out2->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Target mask
        psImageMaskType **source = ro2->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Source mask

        for (int y = yMin; y < yMax; y++) {
            for (int x = xMin; x < xMax; x++) {
                target[y][x] |= source[y][x];
            }
        }
    }

    return true;
}

bool pmSubtractionConvolveThread(psThreadJob *job)
{
    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);

    psArray *args = job->args;          // Arguments
    int numCols = PS_SCALAR_VALUE(args->data[0], S32); // Number of columns
    int numRows = PS_SCALAR_VALUE(args->data[1], S32); // Number of rows
    int x0 = PS_SCALAR_VALUE(args->data[2], S32); // Offset in x
    int y0 = PS_SCALAR_VALUE(args->data[3], S32); // Offset in x
    pmReadout *out1 = args->data[4];    // Output readout 1
    pmReadout *out2 = args->data[5];    // Output readout 2
    psImage *convMask = args->data[6];  // Output convolved mask
    const pmReadout *ro1 = args->data[7]; // Input readout 1
    const pmReadout *ro2 = args->data[8]; // Input readout 2
    psImage *kernelErr1 = args->data[9]; // Kernel error image 1
    psImage *kernelErr2 = args->data[10]; // Kernel error image 2
    psImage *subMask = args->data[11]; // Subtraction mask
    psImageMaskType maskBad = PS_SCALAR_VALUE(args->data[12], PS_TYPE_IMAGE_MASK_DATA); // Output mask value for bad pixels
    psImageMaskType maskPoor = PS_SCALAR_VALUE(args->data[13], PS_TYPE_IMAGE_MASK_DATA); // Output mask value for poor pixels
    float poorFrac = PS_SCALAR_VALUE(args->data[14], F32); // Fraction for "poor"
    const psRegion *region = args->data[15]; // Region to convolve
    const pmSubtractionKernels *kernels = args->data[16]; // Kernels
    bool doBG = PS_SCALAR_VALUE(args->data[17], U8); // Do background subtraction?
    bool useFFT = PS_SCALAR_VALUE(args->data[18], U8); // Use FFT for convolution?

    return subtractionConvolvePatch(numCols, numRows, x0, y0, out1, out2, convMask, ro1, ro2, kernelErr1,
                                    kernelErr2, subMask, maskBad, maskPoor, poorFrac, region, kernels,
                                    doBG, useFFT);
}

bool pmSubtractionConvolve(pmReadout *out1, pmReadout *out2, const pmReadout *ro1, const pmReadout *ro2,
                           psImage *subMask, int stride, psImageMaskType maskBad, psImageMaskType maskPoor,
                           float poorFrac, float kernelError, const psRegion *region,
                           const pmSubtractionKernels *kernels, bool doBG, bool useFFT)
{
    int numCols = 0, numRows = 0;       // Image dimensions
    int x0 = 0, y0 = 0;                 // Image offset
    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        PM_ASSERT_READOUT_NON_NULL(out1, false);
        PM_ASSERT_READOUT_NON_NULL(ro1, false);
        PM_ASSERT_READOUT_IMAGE(ro1, false);
        numCols = ro1->image->numCols;
        numRows = ro1->image->numRows;
        x0 = ro1->col0;
        y0 = ro1->row0;
    }
    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        PM_ASSERT_READOUT_NON_NULL(out2, false);
        PM_ASSERT_READOUT_NON_NULL(ro2, false);
        PM_ASSERT_READOUT_IMAGE(ro2, false);
        if (numCols == 0 && numRows == 0) {
            numCols = ro2->image->numCols;
            numRows = ro2->image->numRows;
            x0 = ro2->col0;
            y0 = ro2->row0;
        }
    }
    if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        PS_ASSERT_IMAGES_SIZE_EQUAL(ro1->image, ro2->image, false);
    }
    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, false);
    if (subMask) {
        PS_ASSERT_IMAGE_NON_NULL(subMask, false);
        PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_IMAGE_MASK, false);
        PS_ASSERT_IMAGE_SIZE(subMask, numCols, numRows, false);
    }
    PS_ASSERT_INT_NONNEGATIVE(stride, false);
    PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(poorFrac, 0.0, false);
    PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(poorFrac, 1.0, false);
    PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(kernelError, 0.0, false);
    PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(kernelError, 1.0, false);
    if (region && psRegionIsNaN(*region)) {
        psString string = psRegionToString(*region);
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Input region (%s) contains NAN values", string);
        psFree(string);
        return false;
    }

    psTimerStart("pmSubtractionConvolve");

    bool threaded = pmSubtractionThreaded(); // Running threaded?

    // Outputs
    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        if (!out1->image) {
            out1->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
        }
        if (ro1->variance) {
            if (!out1->variance) {
                out1->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
            }
            psImageInit(out1->variance, 0.0);
        }
    }
    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        if (!out2->image) {
            out2->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
        }
        if (ro2->variance) {
            if (!out2->variance) {
                out2->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
            }
            psImageInit(out2->variance, 0.0);
        }
    }
    psImage *convMask = NULL;           // Convolved mask image (common to inputs 1 and 2)
    if (subMask) {
        if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
            if (!out1->mask) {
                out1->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
            }
            psImageInit(out1->mask, 0);
            convMask = out1->mask;
        }
        if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
            if (!out2->mask) {
                out2->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
            }
            psImageInit(out2->mask, 0);
            if (!convMask) {
                convMask = out2->mask;
            }
        }
    }

    psImage *kernelErr1 = NULL, *kernelErr2 = NULL; // Kernel error images
#ifdef USE_KERNEL_ERR
    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        kernelErr1 = subtractionKernelErrImage(ro1->image, kernelError);
    }
    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        kernelErr2 = subtractionKernelErrImage(ro2->image, kernelError);
    }
#endif

    int size = kernels->size;           // Half-size of kernel

    // Get region for convolution: [xMin:xMax,yMin:yMax]
    int xMin = size, xMax = numCols - size;
    int yMin = size, yMax = numRows - size;
    if (region) {
        xMin = PS_MAX(region->x0, xMin);
        xMax = PS_MIN(region->x1, xMax);
        yMin = PS_MAX(region->y0, yMin);
        yMax = PS_MIN(region->y1, yMax);
    }

#if 0
    // XXX Use thread-specific data to store these
    psImage *polyValues = NULL;         // Pre-calculated polynomial values
    psKernel *kernelImage = NULL;       // Kernel for the images
    psKernel *kernelVariance = NULL;      // Kernel for the variance maps
#endif

    // Need to turn off threads at the psLib level --- otherwise, we end up with threads on top of threads,
    // and everything is executing psThreadPoolWait, waiting for some other mythical thread to complete the
    // thread's work.
    bool oldThreads = psImageConvolveSetThreads(false); // Old value of threading for psImageConvolve

    if (stride == 0) {
        // Use the full size of the kernel
        stride = 2 * size + 1;
    }

    for (int j = yMin; j < yMax; j += stride) {
        int ySubMax = PS_MIN(j + stride, yMax); // Range for subregion of interest
        for (int i = xMin; i < xMax; i += stride) {
            int xSubMax = PS_MIN(i + stride, xMax); // Range for subregion of interest

            psRegion *subRegion = psRegionAlloc(i, xSubMax, j, ySubMax); // Bounds of subtraction
            if (threaded) {
                psThreadJob *job = psThreadJobAlloc("PSMODULES_SUBTRACTION_CONVOLVE");
                psArray *args = job->args;
                PS_ARRAY_ADD_SCALAR(args, numCols, PS_TYPE_S32);
                PS_ARRAY_ADD_SCALAR(args, numRows, PS_TYPE_S32);
                PS_ARRAY_ADD_SCALAR(args, x0, PS_TYPE_S32);
                PS_ARRAY_ADD_SCALAR(args, y0, PS_TYPE_S32);
                psArrayAdd(args, 1, out1);
                psArrayAdd(args, 1, out2);
                psArrayAdd(args, 1, convMask);
                psArrayAdd(args, 1, (pmReadout*)ro1); // Casting away const
                psArrayAdd(args, 1, (pmReadout*)ro2); // Casting away const
                psArrayAdd(args, 1, kernelErr1);
                psArrayAdd(args, 1, kernelErr2);
                psArrayAdd(args, 1, subMask);
                PS_ARRAY_ADD_SCALAR(args, maskBad, PS_TYPE_IMAGE_MASK);
                PS_ARRAY_ADD_SCALAR(args, maskPoor, PS_TYPE_IMAGE_MASK);
                PS_ARRAY_ADD_SCALAR(args, poorFrac, PS_TYPE_F32);
                psArrayAdd(args, 1, subRegion);
                psArrayAdd(args, 1, (pmSubtractionKernels*)kernels); // Casting away const
                PS_ARRAY_ADD_SCALAR(args, doBG, PS_TYPE_U8);
                PS_ARRAY_ADD_SCALAR(args, useFFT, PS_TYPE_U8);

                if (!psThreadJobAddPending(job)) {
                    psFree(job);
                    return false;
                }
                psFree(job);
            } else {
                subtractionConvolvePatch(numCols, numRows, x0, y0, out1, out2, convMask, ro1, ro2,
                                         kernelErr1, kernelErr2, subMask, maskBad, maskPoor, poorFrac,
                                         subRegion, kernels, doBG, useFFT);
            }
            psFree(subRegion);
        }
    }

    if (!psThreadPoolWait(false)) {
        psError(PS_ERR_UNKNOWN, false, "Error waiting for threads.");
        return false;
    }

    // We don't rely on psThreadPoolWait to harvest the jobs because the job contains a reference to the
    // subMask, which is being changed on a thread, and psThreadPoolWait doesn't know that it needs to be
    // locked before freeing.  After psThreadPoolWait, however, the jobs are completed, the threads are idle,
    // and so there's no need to lock the subMask when we're blowing away the jobs.
    if (threaded) {
        psThreadJob *job;               // Completed job
        while ((job = psThreadJobGetDone())) {
            psAssert(strcmp(job->type, "PSMODULES_SUBTRACTION_CONVOLVE") == 0,
                     "Job has incorrect type: %s", job->type);
            psFree(job);
        }
    }
    psImageConvolveSetThreads(oldThreads);

    psFree(kernelErr1);
    psFree(kernelErr2);

    // Calculate covariances
    // This can take a while, so we only do it for a single instance
    // XXX psImageCovarianceCalculate could be multithreaded
    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        psKernel *kernel = pmSubtractionKernel(kernels, 0.0, 0.0, false); // Convolution kernel
        out1->covariance = psImageCovarianceCalculate(kernel, ro1->covariance);
        psFree(kernel);
    }
    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
        psKernel *kernel = pmSubtractionKernel(kernels, 0.0, 0.0,
                                               kernels->mode == PM_SUBTRACTION_MODE_DUAL); // Conv. kernel
        out2->covariance = psImageCovarianceCalculate(kernel, ro2->covariance);
        psFree(kernel);
    }

    // Copy anything that wasn't convolved
    switch (kernels->mode) {
      case PM_SUBTRACTION_MODE_1:
        if (out2) {
            out2->image = psMemIncrRefCounter(ro2->image);
            out2->variance = psMemIncrRefCounter(ro2->variance);
            out2->mask = psMemIncrRefCounter(ro2->mask);
            out2->covariance = psMemIncrRefCounter(ro2->covariance);
        }
        break;
      case PM_SUBTRACTION_MODE_2:
        if (out1) {
            out1->image = psMemIncrRefCounter(ro1->image);
            out1->variance = psMemIncrRefCounter(ro1->variance);
            out1->mask = psMemIncrRefCounter(ro1->mask);
            out1->covariance = psMemIncrRefCounter(ro1->covariance);
        }
        break;
      case PM_SUBTRACTION_MODE_DUAL:
        break;
      default:
        psAbort("Should never get here.");
    }

    // Data exists on the outputs now
    if (out1) {
        out1->data_exists = true;
        if (out1->parent) {
            out1->parent->data_exists = out1->parent->parent->data_exists = true;
        }
    }
    if (out2) {
        out2->data_exists = true;
        if (out2->parent) {
            out2->parent->data_exists = out2->parent->parent->data_exists = true;
        }
    }

    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Convolve image: %f sec",
             psTimerClear("pmSubtractionConvolve"));

    return true;
}
