/**************************************************************************

   Fotoxx      edit photos and manage collections

   Copyright 2007 2008 2009 2010 2011 2012 Michael Cornelison
   Source URL: http://kornelix.squarespace.com/fotoxx
   Contact: kornelix2@googlemail.com
   
   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 3 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, see http://www.gnu.org/licenses/.

***************************************************************************/

#define EX extern                                                          //  enable extern declarations
#include "fotoxx.h"


/**************************************************************************

   Fotoxx image edit functions - composite functions

***************************************************************************/


//   File scope variables and functions for composite images
//      used by HDR, HDF, STP, STN, Panorama.


int      cimNF;                                                   //  image count, <= 10
char     *cimFile[10];                                            //  image files
PXM      *cimPXMf[10];                                            //  original images
PXM      *cimPXMs[10];                                            //  alignment images, scaled and curved (pano)
PXM      *cimPXMw[10];                                            //  alignment images, warped

struct cimoffs  {                                                 //  image alignment offsets
   double   xf, yf, tf;                                           //  x, y, theta offsets
   double   wx[4], wy[4];                                         //  x/y corner warps, 0=NW, 1=NE, 2=SE, 3=SW
};
cimoffs  cimOffs[10];                                             //  image alignment data in E3 output image

double   cimScale;                                                //  alignment image size relative to full image
double   cimBlend;                                                //  image blend width at overlap, pixels (pano)
double   cimSearchRange;                                          //  alignment search range, pixels
double   cimSearchStep;                                           //  alignment search step, vpixels
double   cimWarpRange;                                            //  alignment corner warp range, pixels
double   cimWarpStep;                                             //  alignment corner warp step, vpixels
double   cimSampSize;                                             //  pixel sample size
int      cimOv1xlo, cimOv1xhi, cimOv1ylo, cimOv1yhi;              //  rectangle enclosing overlap area,
int      cimOv2xlo, cimOv2xhi, cimOv2ylo, cimOv2yhi;              //    image 1 and image2 coordinates
double   cimRGBmf1[3][65536];                                     //  RGB matching factors for pixel comparisons:
double   cimRGBmf2[3][65536];                                     //  cimRGBmf1[*][pix1[*]] == cimRGBmf2[*][pix2[*]]
char     *cimRedpix = 0;                                          //  maps high-contrast pixels for alignment
int      cimRedImage;                                             //  which image has red pixels
int      cimNsearch;                                              //  alignment search counter
int      cimShowIm1, cimShowIm2;                                  //  two images for cim_show_images()
int      cimShowAll;                                              //  if > 0, show all images
int      cimShrink;                                               //  image shrinkage from pano image curving
int      cimPano;                                                 //  pano mode flag for cim_align_image()
int      cimPanoV;                                                //  vertical pano flag

int      cim_load_files();                                        //  load and check selected files
void     cim_scale_image(int im, PXM **);                         //  scale image, 1.0 to cimScale (normally < 1)
double   cim_get_overlap(int im1, int im2, PXM **);               //  get overlap area for images (horiz or vert)
void     cim_match_colors(int im1, int im2, PXM **);              //  match image RGB levels >> match data
void     cim_adjust_colors(PXM *pxm, int fwhich);                 //  adjust RGB levels from match data
void     cim_get_redpix(int im1);                                 //  find high-contrast pixels in overlap area
void     cim_curve_image(int im);                                 //  curve cimPXMs[im] using lens parameters
void     cim_curve_Vimage(int im);                                //  vertical pano version
void     cim_warp_image(int im);                                  //  warp image corners: cimPXMs[im] >> cimPXMw[im]
void     cim_warp_image_pano(int im, int fblend);                 //  pano version, all / left side / blend stripe
void     cim_warp_image_Vpano(int im, int fblend);                //  vertical pans version: bottom side corners
void     cim_align_image(int im1, int im2);                       //  align image im2 to im1, modify im2 offsets
double   cim_match_images(int im1, int im2);                      //  compute match for overlapped images
void     cim_show_images(int fnew, int fblend);                   //  combine images >> E3pxm16 >> main window
void     cim_show_Vimages(int fnew, int fblend);                  //  vertical pano version
void     cim_trim();                                              //  cut-off edges where all images do not overlap
void     cim_dump_offsets(cchar *text);                           //  diagnostic tool


//  load image file into pixmaps cimPXMf[*] and check for errors
//  returns 0 if error

int cim_load_files()                                                       //  v.10.7
{
   PXM      *pxm;

   for (int imx = 0; imx < cimNF; imx++)
   {
      PXM_free(cimPXMf[imx]);
      pxm = f_load(cimFile[imx],16);                                       //  will diagnose errors
      if (! pxm) return 0;
      cimPXMf[imx] = pxm;
      
      PXM_fixblue(pxm);                                                    //  blue=0 >> blue=2 for vpixel()   v.11.07
   }

   return 1;
}


//  scale image from full size) to cimScale (normally < 1.0)

void cim_scale_image(int im, PXM** pxmout)                                 //  v.10.7
{
   int      ww, hh;
   
   ww = cimScale * cimPXMf[im]->ww;
   hh = cimScale * cimPXMf[im]->hh;

   PXM_free(pxmout[im]);   
   pxmout[im] = PXM_rescale(cimPXMf[im],ww,hh);
   
   PXM_fixblue(pxmout[im]);                                                //  blue=0 >> blue=2 for vpixel()   v.11.07

   return;
}


//  get overlap area for a pair of images im1 and im2
//  outputs are coordinates of overlap area in im1 and in im2
//  returns overlap width as fraction of image width <= 1.0

double cim_get_overlap(int im1, int im2, PXM **pxmx)                       //  v.11.04
{
   double      x1, y1, t1, x2, y2, t2;
   double      xoff, yoff, toff, costf, sintf;
   int         ww1, ww2, hh1, hh2, pxM;
   PXM         *pxm1, *pxm2;

   x1 = cimOffs[im1].xf;                                                   //  im1, im2 absolute offsets
   y1 = cimOffs[im1].yf;
   t1 = cimOffs[im1].tf;
   x2 = cimOffs[im2].xf;
   y2 = cimOffs[im2].yf;
   t2 = cimOffs[im2].tf;
   
   xoff = (x2 - x1) * cos(t1) + (y2 - y1) * sin(t1);                       //  offset of im2 relative to im1
   yoff = (y2 - y1) * cos(t1) - (x2 - x1) * sin(t1);
   toff = t2 - t1;

   costf = cos(toff);
   sintf = sin(toff);

   pxm1 = pxmx[im1];
   pxm2 = pxmx[im2];

   ww1 = pxm1->ww;
   hh1 = pxm1->hh;
   ww2 = pxm2->ww;
   hh2 = pxm2->hh;
   
   cimOv1xlo = 0;                                                          //  lowest x overlap
   if (xoff > 0) cimOv1xlo = xoff;

   cimOv1xhi = ww1-1;                                                      //  highest x overlap
   if (cimOv1xhi > xoff + ww2-1) cimOv1xhi = xoff + ww2-1;

   cimOv1ylo = 0;                                                          //  lowest y overlap
   if (yoff > 0) cimOv1ylo = yoff;

   cimOv1yhi = hh1-1;                                                      //  highest y overlap
   if (cimOv1yhi > yoff + hh2-1) cimOv1yhi = yoff + hh2-1;
   
   if (toff < 0) cimOv1xlo -= toff * (cimOv1yhi - cimOv1ylo);              //  reduce for theta offset
   if (toff < 0) cimOv1yhi += toff * (cimOv1xhi - cimOv1xlo);
   if (toff > 0) cimOv1xhi -= toff * (cimOv1yhi - cimOv1ylo);
   if (toff > 0) cimOv1ylo += toff * (cimOv1xhi - cimOv1xlo);
   
   cimOv1xlo += cimShrink + 3;                                             //  account for void areas from
   cimOv1xhi += - cimShrink - 3;                                           //    image shrinkage from 
   cimOv1ylo += cimShrink + 3;                                             //      cim_curve_image()
   cimOv1yhi += - cimShrink - 3;                                           //  v.11.04
   
   if (cimPanoV) {
      if (cimBlend && cimBlend < (cimOv1yhi - cimOv1ylo)) {                //  reduce y range to cimBlend   v.11.04
         pxM = (cimOv1yhi + cimOv1ylo) / 2;
         cimOv1ylo = pxM - cimBlend / 2;
         cimOv1yhi = pxM + cimBlend / 2;
      }
   }
   else {
      if (cimBlend && cimBlend < (cimOv1xhi - cimOv1xlo)) {                //  reduce x range to cimBlend
         pxM = (cimOv1xhi + cimOv1xlo) / 2;
         cimOv1xlo = pxM - cimBlend / 2;
         cimOv1xhi = pxM + cimBlend / 2;
      }
   }

   cimOv2xlo = costf * (cimOv1xlo - xoff) + sintf * (cimOv1ylo - yoff);    //  overlap area in im2 coordinates
   cimOv2xhi = costf * (cimOv1xhi - xoff) + sintf * (cimOv1yhi - yoff);
   cimOv2ylo = costf * (cimOv1ylo - yoff) + sintf * (cimOv1xlo - xoff);
   cimOv2yhi = costf * (cimOv1yhi - yoff) + sintf * (cimOv1xhi - xoff);

   if (cimOv1xlo < 0) cimOv1xlo = 0;                                       //  take care of limits
   if (cimOv1ylo < 0) cimOv1ylo = 0;
   if (cimOv2xlo < 0) cimOv2xlo = 0;
   if (cimOv2ylo < 0) cimOv2ylo = 0;
   if (cimOv1xhi > ww1-1) cimOv1xhi = ww1-1;
   if (cimOv1yhi > hh1-1) cimOv1yhi = hh1-1;
   if (cimOv2xhi > ww2-1) cimOv2xhi = ww2-1;
   if (cimOv2yhi > hh2-1) cimOv2yhi = hh2-1;
   
   if (cimPanoV) return 1.0 * (cimOv1yhi - cimOv1ylo) / hh1;               //  return overlap height <= 1.0  v.11.04
   else return 1.0 * (cimOv1xhi - cimOv1xlo) / ww1;                        //  return overlap width <= 1.0  v.11.03
}


//  Get the RGB brightness distribution in the overlap area for each image.
//  Compute matching factors to compare pixels within the overlap area.
//  compare  cimRGBmf1[rgb][pix1[rgb]]  to  cimRGBmf2[rgb][pix2[rgb]]

void cim_match_colors(int im1, int im2, PXM **pxmx)                        //  v.10.7
{
   double      Bratios1[3][256];                //  image2/image1 brightness ratio per color per level
   double      Bratios2[3][256];                //  image1/image2 brightness ratio per color per level

   uint16      *pix1, vpix2[3];
   int         vstat2, px1, py1;
   int         ii, jj, rgb;
   int         npix, npix1, npix2, npix3;
   int         brdist1[3][256], brdist2[3][256];
   double      x1, y1, t1, x2, y2, t2;
   double      xoff, yoff, toff, costf, sintf;
   double      px2, py2;
   double      brlev1[3][256], brlev2[3][256];
   double      a1, a2, b1, b2, bratio = 1;
   double      r256 = 1.0 / 256.0;
   PXM         *pxm1, *pxm2;
   
   pxm1 = pxmx[im1];
   pxm2 = pxmx[im2];
   
   x1 = cimOffs[im1].xf;                                                   //  im1, im2 absolute offsets
   y1 = cimOffs[im1].yf;
   t1 = cimOffs[im1].tf;
   x2 = cimOffs[im2].xf;
   y2 = cimOffs[im2].yf;
   t2 = cimOffs[im2].tf;
   
   xoff = (x2 - x1) * cos(t1) + (y2 - y1) * sin(t1);                       //  offset of im2 relative to im1
   yoff = (y2 - y1) * cos(t1) - (x2 - x1) * sin(t1);
   toff = t2 - t1;

   costf = cos(toff);
   sintf = sin(toff);

   for (rgb = 0; rgb < 3; rgb++)                                           //  clear distributions
   for (ii = 0; ii < 256; ii++)
      brdist1[rgb][ii] = brdist2[rgb][ii] = 0;

   npix = 0;

   for (py1 = cimOv1ylo; py1 < cimOv1yhi; py1++)                           //  loop overlapped rows
   for (px1 = cimOv1xlo; px1 < cimOv1xhi; px1++)                           //  loop overlapped columns
   {
      pix1 = PXMpix(pxm1,px1,py1);                                         //  image1 pixel
      if (! pix1[2]) continue;                                             //  ignore void pixels

      px2 = costf * (px1 - xoff) + sintf * (py1 - yoff);                   //  corresponding image2 pixel
      py2 = costf * (py1 - yoff) - sintf * (px1 - xoff);
      vstat2 = vpixel(pxm2,px2,py2,vpix2);
      if (! vstat2) continue;                                              //  does not exist

      ++npix;                                                              //  count overlapping pixels
      
      for (rgb = 0; rgb < 3; rgb++)                                        //  accumulate distributions
      {                                                                    //    by color in 256 bins
         ++brdist1[rgb][int(r256*pix1[rgb])];
         ++brdist2[rgb][int(r256*vpix2[rgb])];
      }
   }
   
   npix1 = npix / 256;                                                     //  1/256th of total pixels
   
   for (rgb = 0; rgb < 3; rgb++)                                           //  get brlev1[rgb][N] = mean bright
   for (ii = jj = 0; jj < 256; jj++)                                       //    for Nth group of image1 pixels
   {                                                                       //      for color rgb
      brlev1[rgb][jj] = 0;
      npix2 = npix1;                                                       //  1/256th of total pixels

      while (npix2 > 0 && ii < 256)                                        //  next 1/256th group from distr,
      {
         npix3 = brdist1[rgb][ii];
         if (npix3 == 0) { ++ii; continue; }
         if (npix3 > npix2) npix3 = npix2;
         brlev1[rgb][jj] += ii * npix3;                                    //  brightness * (pixels with)
         brdist1[rgb][ii] -= npix3;
         npix2 -= npix3;
      }

      brlev1[rgb][jj] = brlev1[rgb][jj] / npix1;                           //  mean brightness for group, 0-255
   }

   for (rgb = 0; rgb < 3; rgb++)                                           //  do same for image2
   for (ii = jj = 0; jj < 256; jj++)
   {
      brlev2[rgb][jj] = 0;
      npix2 = npix1;

      while (npix2 > 0 && ii < 256)
      {
         npix3 = brdist2[rgb][ii];
         if (npix3 == 0) { ++ii; continue; }
         if (npix3 > npix2) npix3 = npix2;
         brlev2[rgb][jj] += ii * npix3;
         brdist2[rgb][ii] -= npix3;
         npix2 -= npix3;
      }

      brlev2[rgb][jj] = brlev2[rgb][jj] / npix1;
   }

   for (rgb = 0; rgb < 3; rgb++)                                           //  color
   for (ii = jj = 0; ii < 256; ii++)                                       //  brlev1 brightness, 0 to 255
   {
      if (ii == 0) bratio = 1;
      while (ii > brlev2[rgb][jj] && jj < 256) ++jj;                       //  find matching brlev2 brightness
      a2 = brlev2[rgb][jj];                                                //  next higher value
      b2 = brlev1[rgb][jj];
      if (a2 > 0 && b2 > 0) {
         if (jj > 0) {
            a1 = brlev2[rgb][jj-1];                                        //  next lower value
            b1 = brlev1[rgb][jj-1];
         }
         else   a1 = b1 = 0;
         if (ii == 0)  bratio = b2 / a2;
         else   bratio = (b1 + (ii-a1)/(a2-a1) * (b2-b1)) / ii;            //  interpolate
      }

      if (bratio < 0.2) bratio = 0.2;                                      //  contain outliers
      if (bratio > 5) bratio = 5;
      Bratios2[rgb][ii] = bratio;
   }

   for (rgb = 0; rgb < 3; rgb++)                                           //  color
   for (ii = jj = 0; ii < 256; ii++)                                       //  brlev2 brightness, 0 to 255
   {
      if (ii == 0) bratio = 1;
      while (ii > brlev1[rgb][jj] && jj < 256) ++jj;                       //  find matching brlev1 brightness
      a2 = brlev1[rgb][jj];                                                //  next higher value
      b2 = brlev2[rgb][jj];
      if (a2 > 0 && b2 > 0) {
         if (jj > 0) {
            a1 = brlev1[rgb][jj-1];                                        //  next lower value
            b1 = brlev2[rgb][jj-1];
         }
         else   a1 = b1 = 0;
         if (ii == 0)  bratio = b2 / a2;
         else   bratio = (b1 + (ii-a1)/(a2-a1) * (b2-b1)) / ii;            //  interpolate
      }

      if (bratio < 0.2) bratio = 0.2;                                      //  contain outliers
      if (bratio > 5) bratio = 5;
      Bratios1[rgb][ii] = bratio;
   }
   
   for (ii = 0; ii < 65536; ii++)                                          //  convert brightness ratios into
   {                                                                       //    conversion factors
      jj = ii / 256;

      for (rgb = 0; rgb < 3; rgb++)
      {
         cimRGBmf1[rgb][ii] = sqrt(Bratios1[rgb][jj]) * ii;                //  use sqrt(ratio) so that adjustment
         cimRGBmf2[rgb][ii] = sqrt(Bratios2[rgb][jj]) * ii;                //    can be applied to both images
      }
   }
   
   return;
}


//  Use color match data from cim_match_colors() to 
//    modify images so the colors match.

void cim_adjust_colors(PXM *pxm, int fwhich)                               //  v.10.7
{
   int         ww, hh, px, py;
   int         red, green, blue, max;
   uint16      *pix;
   double      f1;
   
   ww = pxm->ww;
   hh = pxm->hh;

   for (py = 0; py < hh; py++)
   for (px = 0; px < ww; px++)
   {
      pix = PXMpix(pxm,px,py);
      red = pix[0];
      green = pix[1];
      blue = pix[2];
      if (! blue) continue;

      if (fwhich == 1) {
         red = cimRGBmf1[0][red];
         green = cimRGBmf1[1][green];
         blue = cimRGBmf1[2][blue];
      }

      if (fwhich == 2) {
         red = cimRGBmf2[0][red];
         green = cimRGBmf2[1][green];
         blue = cimRGBmf2[2][blue];
      }

      if (red > 65535 || green > 65535 || blue > 65535) {
         max = red;
         if (green > max) max = green;
         if (blue > max) max = blue;
         f1 = 65535.0 / max;
         red = red * f1;
         green = green * f1;
         blue = blue * f1;
      }
      
      if (! blue) blue = 1;                                                //  avoid 0    v.10.7

      pix[0] = red;
      pix[1] = green;
      pix[2] = blue;
   }

   return;
}


//  find pixels of greatest contrast within overlap area
//  flag high-contrast pixels to use in each image compare region

void cim_get_redpix(int im1)                                               //  v.10.7
{
   int         ww, hh, samp, xzone, yzone;
   int         pxL, pxH, pyL, pyH;
   int         px, py, ii, jj, npix;
   int         red1, green1, blue1, red2, green2, blue2, tcon;
   int         ov1xlo, ov1xhi, ov1ylo, ov1yhi;
   int         Hdist[256], Vdist[256], Hmin, Vmin;
   double      s8 = 1.0 / 770.0;
   double      zsamp[16] = { 4,6,6,4,6,9,9,6,6,9,9,6,4,6,6,4 };            //  % sample per zone, sum = 100
   uchar       *Hcon, *Vcon;
   uint16      *pix1, *pix2;
   PXM         *pxm;
   
   pxm = cimPXMs[im1];                                                     //  v.11.04
   ww = pxm->ww;
   hh = pxm->hh;

   if (cimRedpix) zfree(cimRedpix);                                        //  clear prior
   cimRedpix = zmalloc(ww*hh,"cimRedpix");
   memset(cimRedpix,0,ww*hh);

   cimRedImage = im1;                                                      //  image with red pixels
   
   ov1xlo = cimOv1xlo + cimSearchRange;                                    //  stay within x/y search range
   ov1xhi = cimOv1xhi - cimSearchRange;                                    //    so that red pixels persist
   ov1ylo = cimOv1ylo + cimSearchRange;                                    //      over offset changes
   ov1yhi = cimOv1yhi - cimSearchRange;
   
   for (yzone = 0; yzone < 4; yzone++)                                     //  loop 16 zones       v.10.8
   for (xzone = 0; xzone < 4; xzone++)
   {
      pxL = ov1xlo + 0.25 * xzone     * (ov1xhi - ov1xlo);                 //  px and py zone limits
      pxH = ov1xlo + 0.25 * (xzone+1) * (ov1xhi - ov1xlo);
      pyL = ov1ylo + 0.25 * yzone     * (ov1yhi - ov1ylo);
      pyH = ov1ylo + 0.25 * (yzone+1) * (ov1yhi - ov1ylo);
      
      npix = (pxH - pxL) * (pyH - pyL);                                    //  zone pixels
      Hcon = (uchar *) zmalloc(npix,"cimRedpix");                          //  horizontal pixel contrast 0-255
      Vcon = (uchar *) zmalloc(npix,"cimRedpix");                          //  vertical pixel contrast 0-255
      
      ii = 4 * yzone + xzone;
      samp = cimSampSize * 0.01 * zsamp[ii];                               //  sample size for zone
      if (samp > 0.1 * npix) samp = 0.1 * npix;                            //  limit to 10% of zone pixels
      
      for (py = pyL; py < pyH; py++)                                       //  scan image pixels in zone
      for (px = pxL; px < pxH; px++)
      {
         ii = (py-pyL) * (pxH-pxL) + (px-pxL);
         Hcon[ii] = Vcon[ii] = 0;                                          //  horiz. = vert. contrast = 0

         if (py < 8 || py > hh-9) continue;                                //  keep away from image edges
         if (px < 8 || px > ww-9) continue;

         pix1 = PXMpix(pxm,px,py-6);                                       //  verify not near void areas
         if (! pix1[2]) continue;
         pix1 = PXMpix(pxm,px+6,py);
         if (! pix1[2]) continue;
         pix1 = PXMpix(pxm,px,py+6);
         if (! pix1[2]) continue;
         pix1 = PXMpix(pxm,px-6,py);
         if (! pix1[2]) continue;

         pix1 = PXMpix(pxm,px,py);                                         //  candidate red pixel
         red1 = pix1[0];
         green1 = pix1[1];
         blue1 = pix1[2];

         pix2 = PXMpix(pxm,px+2,py);                                       //  2 pixels to right
         red2 = pix2[0];
         green2 = pix2[1];
         blue2 = pix2[2];

         tcon = abs(red1-red2) + abs(green1-green2) + abs(blue1-blue2);    //  horizontal contrast
         Hcon[ii] = int(tcon * s8);                                        //  scale  0 - 255

         pix2 = PXMpix(pxm,px,py+2);                                       //  2 pixels below
         red2 = pix2[0];
         green2 = pix2[1];
         blue2 = pix2[2];

         tcon = abs(red1-red2) + abs(green1-green2) + abs(blue1-blue2);    //  vertical contrast
         Vcon[ii] = int(tcon * s8);
      }

      for (ii = 0; ii < 256; ii++) Hdist[ii] = Vdist[ii] = 0;              //  clear contrast distributions

      for (py = pyL; py < pyH; py++)                                       //  scan image pixels
      for (px = pxL; px < pxH; px++)
      {                                                                    //  build contrast distributions
         ii = (py-pyL) * (pxH-pxL) + (px-pxL);
         ++Hdist[Hcon[ii]];
         ++Vdist[Vcon[ii]];
      }
      
      for (npix = 0, ii = 255; ii > 0; ii--)                               //  find minimum contrast needed to get
      {                                                                    //    enough pixels for sample size
         npix += Hdist[ii];                                                //      (horizontal contrast pixels)
         if (npix > samp) break; 
      }
      Hmin = ii; 

      for (npix = 0, ii = 255; ii > 0; ii--)                               //  (verticle contrast pixels)
      {
         npix += Vdist[ii];
         if (npix > samp) break;
      }
      Vmin = ii;

      for (py = pyL; py < pyH; py++)                                       //  scan zone pixels
      for (px = pxL; px < pxH; px++)
      {
         ii = (py-pyL) * (pxH-pxL) + (px-pxL);
         jj = py * ww + px;
         if (Hcon[ii] > Hmin) cimRedpix[jj] = 1;                           //  flag pixels above min. contrast
         if (Vcon[ii] > Vmin) cimRedpix[jj] = 1;
      }

      zfree(Hcon);
      zfree(Vcon);

      for (py = pyL; py < pyH; py++)                                       //  scan zone pixels
      for (px = pxL; px < pxH; px++)
      {
         ii = (py-pyL) * (pxH-pxL) + (px-pxL);
         jj = py * ww + px;
         if (! cimRedpix[jj]) continue;
         npix = cimRedpix[jj-1] + cimRedpix[jj+1];                         //  eliminate flagged pixels with no
         npix += cimRedpix[jj-ww] + cimRedpix[jj+ww];                      //    neighboring flagged pixels
         npix += cimRedpix[jj-ww-1] + cimRedpix[jj+ww-1];                  //  v.11.03
         npix += cimRedpix[jj-ww+1] + cimRedpix[jj+ww+1];
         if (npix < 2) cimRedpix[jj] = 0;
      }

      for (py = pyL; py < pyH; py++)                                       //  scan zone pixels
      for (px = pxL; px < pxH; px++)
      {
         ii = (py-pyL) * (pxH-pxL) + (px-pxL);
         jj = py * ww + px;

         if (cimRedpix[jj] == 1) {                                         //  flag horizontal group of 3
            cimRedpix[jj+1] = 2;
            cimRedpix[jj+2] = 2;
            cimRedpix[jj+ww] = 2;                                          //  and vertical group of 3
            cimRedpix[jj+2*ww] = 2;
         }
      }
   }
   
   return;
}


//  curve image based on lens parameters (pano)
//  replaces cimPXMs[im] with curved version

void cim_curve_image(int im)                                               //  overhauled    v.11.03
{
   int         px, py, ww, hh, vstat;
   double      ww2, hh2;
   double      dx, dy;
   double      F = lens_mm;                                                //  lens focal length, 35mm equivalent
   double      S = 35.0;                                                   //  corresponding image width
   double      R1, R2, G, T, bow;
   PXM         *pxmin, *pxmout;
   uint16      vpix[3], *pix;
   
   pxmin = cimPXMs[im];                                                    //  input and output image
   ww = pxmin->ww;                  //    200
   hh = pxmin->hh;
   ww2 = 0.5 * ww;                  //    100
   hh2 = 0.5 * hh;
   
   if (hh > ww) S = S * ww / hh;                                           //  vertical format
   F = F / S;                       //    28 / 35                          //  scale to image dimensions
   S = ww2;                         //    100
   F = F * ww;                      //    160
   R1 = F;                                                                 //  cylinder tangent to image plane
   
   bow = -lens_bow * 0.01 / hh2 / hh2;                                     //  lens bow % to fraction
   if (hh > ww) 
      bow = -lens_bow * 0.01 / ww2 / ww2;

   pxmout = PXM_make(ww,hh,16);                                            //  temp. output PXM
   
   for (py = 0; py < hh; py++)                                             //  cylindrical projection    v.11.03
   for (px = 0; px < ww; px++)
   {
      dx = px - ww2;
      dy = py - hh2;
      T = dx / R1;
      dx = F * tan(T);
      R2 = sqrt(dx * dx + F * F);
      G = R1 - R2;
      dy = (dy * R2) / (R2 + G);
      dx += bow * dx * dy * dy;                                            //  barrel distortion
      dx += ww2;
      dy += hh2;
      vstat = vpixel(pxmin,dx,dy,vpix);                                    //  input virtual pixel
      pix = PXMpix(pxmout,px,py);                                          //  output real pixel
      if (vstat) {  
         pix[0] = vpix[0];
         pix[1] = vpix[1];
         pix[2] = vpix[2];
      }
      else pix[0] = pix[1] = pix[2] = 0;                                   //  voided pixels are (0,0,0)
   }

   for (px = 1; px < ww2; px++) {                                          //  compute image shrinkage
      pix = PXMpix(pxmout,px,hh/2);
      if (pix[2]) break;
   }
   cimShrink = px-1;                                                       //  = 0 if no curvature
   
   PXM_free(pxmin);                                                        //  replace input with output PXM
   cimPXMs[im] = pxmout;

   return;
}


//  version for vertical panorama

void cim_curve_Vimage(int im)                                              //  v.11.04
{
   int         px, py, ww, hh, vstat;
   double      ww2, hh2;
   double      dx, dy;
   double      F = lens_mm;                                                //  lens focal length, 35mm equivalent
   double      S = 35.0;                                                   //  corresponding image width
   double      R1, R2, G, T, bow;
   PXM         *pxmin, *pxmout;
   uint16      vpix[3], *pix;
   
   pxmin = cimPXMs[im];                                                    //  input and output image
   ww = pxmin->ww;                  //    200
   hh = pxmin->hh;
   ww2 = 0.5 * ww;                  //    100
   hh2 = 0.5 * hh;
   
   if (hh > ww) S = S * ww / hh;                                           //  vertical format
   F = F / S;                       //    28 / 35                          //  scale to image dimensions
   S = ww2;                         //    100
   F = F * ww;                      //    160
   R1 = F;                                                                 //  cylinder tangent to image plane
   
   bow = -lens_bow * 0.01 / hh2 / hh2;                                     //  lens bow % to fraction
   if (hh > ww) 
      bow = -lens_bow * 0.01 / ww2 / ww2;

   pxmout = PXM_make(ww,hh,16);                                            //  temp. output PXM
   
   for (py = 0; py < hh; py++)                                             //  cylindrical projection    v.11.03
   for (px = 0; px < ww; px++)
   {
      dx = px - ww2;
      dy = py - hh2;
      T = dy / R1;
      dy = F * tan(T);
      R2 = sqrt(dy * dy + F * F);
      G = R1 - R2;
      dx = (dx * R2) / (R2 + G);
      dy += bow * dy * dx * dx;                                            //  barrel distortion
      dx += ww2;
      dy += hh2;
      vstat = vpixel(pxmin,dx,dy,vpix);                                    //  input virtual pixel
      pix = PXMpix(pxmout,px,py);                                          //  output real pixel
      if (vstat) {  
         pix[0] = vpix[0];
         pix[1] = vpix[1];
         pix[2] = vpix[2];
      }
      else pix[0] = pix[1] = pix[2] = 0;                                   //  voided pixels are (0,0,0)
   }

   for (py = 1; py < hh2; py++) {                                          //  compute image shrinkage
      pix = PXMpix(pxmout,ww/2,py);
      if (pix[2]) break;
   }
   cimShrink = py-1;                                                       //  = 0 if no curvature
   
   PXM_free(pxmin);                                                        //  replace input with output PXM
   cimPXMs[im] = pxmout;

   return;
}


//  Warp 4 image corners according to cimOffs[im].wx[ii] and .wy[ii]
//  corner = 0 = NW,  1 = NE,  2 = SE,  3 = SW
//  4 corners move by these pixel amounts and center does not move.
//  input: cimPXMs[im] (flat or curved) output: cimPXMw[im]

namespace cim_warp_image_names {
   PXM         *pxmin, *pxmout;
   double      ww, hh, wwi, hhi;
   double      wx0, wy0, wx1, wy1, wx2, wy2, wx3, wy3;
}

void cim_warp_image(int im)                                                //  caller function     v.10.8
{
   using namespace cim_warp_image_names;

   void * cim_warp_image_wthread(void *arg);
   
   pxmin = cimPXMs[im];                                                    //  input and output pixmaps
   pxmout = cimPXMw[im];
   
   PXM_free(pxmout);                                                       //  v.11.04
   pxmout = PXM_copy(pxmin);
   cimPXMw[im] = pxmout;

   ww = pxmin->ww;
   hh = pxmin->hh;
   wwi = 1.0 / ww;
   hhi = 1.0 / hh;

   wx0 = cimOffs[im].wx[0];                                                //  corner warps
   wy0 = cimOffs[im].wy[0];
   wx1 = cimOffs[im].wx[1];
   wy1 = cimOffs[im].wy[1];
   wx2 = cimOffs[im].wx[2];
   wy2 = cimOffs[im].wy[2];
   wx3 = cimOffs[im].wx[3];
   wy3 = cimOffs[im].wy[3];

   for (int ii = 0; ii < Nwt; ii++)                                        //  start worker threads
      start_wthread(cim_warp_image_wthread,&wtnx[ii]);
   wait_wthreads();                                                        //  wait for completion

   return;
}


void * cim_warp_image_wthread(void *arg)                                   //  worker thread function  v.10.8
{
   using namespace cim_warp_image_names;

   int         index = *((int *) arg);
   int         pxm, pym, vstat;
   uint16      vpix[3], *pixm;
   double      px, py, dx, dy, coeff;
   
   for (pym = index; pym < hh; pym += Nwt)                                 //  loop all pixels for this thread
   for (pxm = 0; pxm < ww; pxm++)
   {
      dx = dy = 0.0;

      coeff = (1.0 - pym * hhi - pxm * wwi);                               //  corner 0  NW
      if (coeff > 0) {
         dx += coeff * wx0;
         dy += coeff * wy0;
      }
      coeff = (1.0 - pym * hhi - (ww - pxm) * wwi);                        //  corner 1  NE
      if (coeff > 0) {
         dx += coeff * wx1;
         dy += coeff * wy1;
      }
      coeff = (1.0 - (hh - pym) * hhi - (ww - pxm) * wwi);                 //  corner 2  SE
      if (coeff > 0) {
         dx += coeff * wx2;
         dy += coeff * wy2;
      }
      coeff = (1.0 - (hh - pym) * hhi - pxm * wwi);                        //  corner 3  SW
      if (coeff > 0) {
         dx += coeff * wx3;
         dy += coeff * wy3;
      }

      px = pxm + dx;                                                       //  source pixel location
      py = pym + dy;

      vstat = vpixel(pxmin,px,py,vpix);                                    //  input virtual pixel
      pixm = PXMpix(pxmout,pxm,pym);                                       //  output real pixel

      if (vstat) {  
         pixm[0] = vpix[0];
         pixm[1] = vpix[1];
         pixm[2] = vpix[2];
      }
      else pixm[0] = pixm[1] = pixm[2] = 0;
   }
   
   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


//  warp image for pano, left side corners only, reduced warp range
//  input: cimPXMs[im] (curved) 
//  output: cimPXMw[im] (warped)
//  fblend: 0 = process entire image
//          1 = process left half only
//          2 = process blend stripe only

void cim_warp_image_pano(int im, int fblend)                               //  v.10.8
{
   int         ww, hh, ww2, hh2, pxL, pxH;
   int         pxm, pym, vstat;
   uint16      vpix[3], *pixm;
   double      ww2i, hh2i, pxs, pys, xdisp, ydisp;
   double      wx0, wy0, wx3, wy3;
   PXM         *pxmin, *pxmout;
   
   pxmin = cimPXMs[im];                                                    //  input and output pixmaps
   pxmout = cimPXMw[im];
   
   PXM_free(pxmout);                                                       //  v.11.04
   pxmout = PXM_copy(pxmin);
   cimPXMw[im] = pxmout;

   ww = pxmin->ww;
   hh = pxmin->hh;

   ww2 = ww / 2;
   hh2 = hh / 2;

   ww2i = 1.0 / ww2;                                                       //  v.10.8
   hh2i = 1.0 / hh2;

   wx0 = cimOffs[im].wx[0];                                                //  NW corner warp
   wy0 = cimOffs[im].wy[0];

   wx3 = cimOffs[im].wx[3];                                                //  SW corner warp
   wy3 = cimOffs[im].wy[3];

   pxL = 0;                                                                //  entire image        v.10.8
   pxH = ww;
   
   if (fblend == 1)                                                        //  left half
      pxH = ww2;

   if (fblend == 2) {
      pxL = cimOv2xlo;                                                     //  limit to overlap/blend width
      pxH = cimOv2xhi;
   }
   
   for (pym = 0; pym < hh; pym++)                                          //  loop all output pixels
   for (pxm = pxL; pxm < pxH; pxm++)
   {
      pixm = PXMpix(pxmout,pxm,pym);                                       //  output pixel
      
      xdisp = (pxm - ww2) * ww2i;                                          //  -1 ... 0 ... +1
      ydisp = (pym - hh2) * hh2i;                                          //  v.11.04
      
      if (xdisp > 0) {                                                     //  right half, no warp
         pxs = pxm;
         pys = pym;
      }
      else if (ydisp < 0) {                                                //  use NW corner warp
         pxs = pxm + wx0 * xdisp * ydisp;
         pys = pym + wy0 * xdisp * ydisp;
      }
      else {                                                               //  use SW corner warp
         pxs = pxm + wx3 * xdisp * ydisp;
         pys = pym + wy3 * xdisp * ydisp;
      }

      vstat = vpixel(pxmin,pxs,pys,vpix);                                  //  input virtual pixel

      if (vstat) {  
         pixm[0] = vpix[0];
         pixm[1] = vpix[1];
         pixm[2] = vpix[2];
      }
      else pixm[0] = pixm[1] = pixm[2] = 0;
   }

   for (pxm = 1; pxm < ww2; pxm++) {                                       //  compute image shrinkage
      pixm = PXMpix(pxmout,pxm,hh2);                                       //    used by cim_get_overlap()
      if (pixm[2]) break;
   }
   cimShrink = pxm-1;

   return;
}


//  vertical pano version - warp top side corners (NW, NE)

void cim_warp_image_Vpano(int im, int fblend)                              //  v.11.04
{
   int         ww, hh, ww2, hh2, pyL, pyH;
   int         pxm, pym, vstat;
   uint16      vpix[3], *pixm;
   double      ww2i, hh2i, pxs, pys, xdisp, ydisp;
   double      wx0, wy0, wx1, wy1;
   PXM         *pxmin, *pxmout;
   
   pxmin = cimPXMs[im];                                                    //  input and output pixmaps
   pxmout = cimPXMw[im];
   
   PXM_free(pxmout);                                                       //  v.11.04
   pxmout = PXM_copy(pxmin);
   cimPXMw[im] = pxmout;

   ww = pxmin->ww;
   hh = pxmin->hh;

   ww2 = ww / 2;
   hh2 = hh / 2;

   ww2i = 1.0 / ww2;                                                       //  v.10.8
   hh2i = 1.0 / hh2;

   wx0 = cimOffs[im].wx[0];                                                //  NW corner warp
   wy0 = cimOffs[im].wy[0];

   wx1 = cimOffs[im].wx[1];                                                //  NE corner warp
   wy1 = cimOffs[im].wy[1];

   pyL = 0;                                                                //  entire image        v.10.8
   pyH = hh;
   
   if (fblend == 1)                                                        //  top half
      pyH = hh2;

   if (fblend == 2) {
      pyL = cimOv2ylo;                                                     //  limit to overlap/blend width
      pyH = cimOv2yhi;
   }
   
   for (pym = pyL; pym < pyH; pym++)                                       //  loop all output pixels
   for (pxm = 0; pxm < ww; pxm++)
   {
      pixm = PXMpix(pxmout,pxm,pym);                                       //  output pixel
      
      xdisp = (pxm - ww2) * ww2i;                                          //  -1 ... 0 ... +1
      ydisp = (pym - hh2) * hh2i;
      
      if (ydisp > 0) {                                                     //  bottom half, no warp
         pxs = pxm;
         pys = pym;
      }
      else if (xdisp < 0) {                                                //  use NW corner warp
         pxs = pxm + wx0 * xdisp * ydisp;
         pys = pym + wy0 * xdisp * ydisp;
      }
      else {                                                               //  use NE corner warp
         pxs = pxm + wx1 * xdisp * ydisp;
         pys = pym + wy1 * xdisp * ydisp;
      }

      vstat = vpixel(pxmin,pxs,pys,vpix);                                  //  input virtual pixel

      if (vstat) {  
         pixm[0] = vpix[0];
         pixm[1] = vpix[1];
         pixm[2] = vpix[2];
      }
      else pixm[0] = pixm[1] = pixm[2] = 0;
   }

   for (pym = 1; pym < hh2; pym++) {                                       //  compute image shrinkage
      pixm = PXMpix(pxmout,ww2,pym);                                       //    used by cim_get_overlap()
      if (pixm[2]) break;
   }
   cimShrink = pym-1;

   return;
}


//  fine-align a pair of images im1 and im2
//  cimPXMs[im2] is aligned with cimPXMs[im1]
//  inputs are cimOffs[im1] and cimOffs[im2] (x/y/t and corner offsets)
//  output is adjusted offsets and corner warp values for im2 only
//  (im1 is used as-is without corner warps)

void cim_align_image(int im1, int im2)                                     //  speedup v.11.03
{
   int         ii, corner1, cornerstep, cornerN, pass;
   double      xyrange, xystep, trange, tstep, wrange, wstep;
   double      xfL, xfH, yfL, yfH, tfL, tfH;
   double      wxL, wxH, wyL, wyH;
   double      match, matchB;
   cimoffs     offsets0, offsetsB;
   
   offsets0 = cimOffs[im2];                                                //  initial offsets
   offsetsB = offsets0;                                                    //  = best offsets so far
   matchB = cim_match_images(im1,im2);                                     //  = best image match level

   if (cimPanoV) cim_show_Vimages(0,0);                                    //  v.11.04
   else cim_show_images(0,0);                                              //  show with 50/50 blend

   for (pass = 1; pass <=2; pass++)                                        //  main pass and 2nd pass    v.10.8
   {
      xyrange = cimSearchRange;                                            //  x/y search range and step
      xystep = cimSearchStep;   

      trange = xyrange / (cimOv1yhi - cimOv1ylo);                          //  angle range, radians
      tstep = trange * xystep / xyrange;
      
      if (pass == 2) {
         xyrange = 0.5 * xyrange;                                          //  2nd pass, reduce range and step
         xystep = 0.5 * xystep;                                            //  v.11.04
         trange = 0.5 * trange;
         tstep = 0.5 * tstep;
      }
      
      //  search x/y/t range for best match

      xfL = cimOffs[im2].xf - xyrange;
      xfH = cimOffs[im2].xf + xyrange + 0.5 * xystep;
      yfL = cimOffs[im2].yf - xyrange;
      yfH = cimOffs[im2].yf + xyrange + 0.5 * xystep;
      tfL = cimOffs[im2].tf - trange;
      tfH = cimOffs[im2].tf + trange + 0.5 * tstep;

      for (cimOffs[im2].xf = xfL; cimOffs[im2].xf < xfH; cimOffs[im2].xf += xystep)
      for (cimOffs[im2].yf = yfL; cimOffs[im2].yf < yfH; cimOffs[im2].yf += xystep)
      for (cimOffs[im2].tf = tfL; cimOffs[im2].tf < tfH; cimOffs[im2].tf += tstep)
      {
         match = cim_match_images(im1,im2);                                //  get match level

         if (sigdiff(match,matchB,0.00001) > 0) {
            matchB = match;                                                //  save best match
            offsetsB = cimOffs[im2];
         }

         sprintf(SB_text,"align: %d  match: %.5f",cimNsearch++,matchB);    //  update status bar
         zmainloop();                                                      //  v.11.11.1
      }
      
      cimOffs[im2] = offsetsB;                                             //  restore best match

      if (cimPanoV) cim_show_Vimages(0,0);                                 //  v.11.04
      else cim_show_images(0,0);
      
      //  warp corners and search for best match

      wrange = cimWarpRange;                                               //  corner warp range and step
      wstep = cimWarpStep;
      if (! wrange) continue;

      if (pass == 2) {                                                     //  2nd pass, 1/4 range and 1/2 step
         wrange = wrange / 4;
         wstep = wstep / 2;
      }
      
      corner1 = 0;                                                         //  process all 4 corners
      cornerN = 3;
      cornerstep = 1;

      if (cimPano) {
         corner1 = 0;                                                      //  left side corners 0, 3
         cornerN = 3;
         cornerstep = 3;
      }
      
      if (cimPanoV) {
         corner1 = 0;                                                      //  top side corners 0, 1   v.11.04
         cornerN = 1;
         cornerstep = 1;
      }

      matchB = cim_match_images(im1,im2);                                  //  initial image match level
      
      for (ii = corner1; ii <= cornerN; ii += cornerstep)                  //  modify one corner at a time
      {
         wxL = cimOffs[im2].wx[ii] - wrange;
         wxH = cimOffs[im2].wx[ii] + wrange + 0.5 * wstep;
         wyL = cimOffs[im2].wy[ii] - wrange;
         wyH = cimOffs[im2].wy[ii] + wrange + 0.5 * wstep;

         for (cimOffs[im2].wx[ii] = wxL; cimOffs[im2].wx[ii] < wxH; cimOffs[im2].wx[ii] += wstep)
         for (cimOffs[im2].wy[ii] = wyL; cimOffs[im2].wy[ii] < wyH; cimOffs[im2].wy[ii] += wstep)
         {
            match = cim_match_images(im1,im2);                             //  get match level

            if (sigdiff(match,matchB,0.00001) > 0) {
               matchB = match;                                             //  save best match
               offsetsB = cimOffs[im2];
            }

            sprintf(SB_text,"warp: %d  match: %.5f",cimNsearch++,matchB);
            zmainloop();                                                   //  v.11.11.1
         }

         cimOffs[im2] = offsetsB;                                          //  restore best match
      }
      
      if (cimPano) cim_warp_image_pano(im2,1);                             //  apply corner warps     v.11.04
      else if (cimPanoV) cim_warp_image_Vpano(im2,1);
      else  cim_warp_image(im2);

      if (cimPanoV) cim_show_Vimages(0,0);                                 //  v.11.04
      else cim_show_images(0,0);
   }

   return;
}


//  Compare 2 pixels using precalculated brightness ratios
//  1.0 = perfect match   0 = total mismatch (black/white)

inline double cim_match_pixels(uint16 *pix1, uint16 *pix2)                 //  v.10.7
{
   double      red1, green1, blue1, red2, green2, blue2;
   double      reddiff, greendiff, bluediff, match;
   double      ff = 1.0 / 65536.0;

   red1 = pix1[0];
   green1 = pix1[1];
   blue1 = pix1[2];

   red2 = pix2[0];
   green2 = pix2[1];
   blue2 = pix2[2];

   reddiff = ff * fabs(red1-red2);                                         //  0 = perfect match
   greendiff = ff * fabs(green1-green2);                                   //  1 = total mismatch
   bluediff = ff * fabs(blue1-blue2);
   
   match = (1.0 - reddiff) * (1.0 - greendiff) * (1.0 - bluediff);         //  1 = perfect match
   return match;
}


//  Compare two images in overlapping areas.
//  Use the high-contrast pixels from cim_get_redpix()
//  return: 1 = perfect match, 0 = total mismatch (black/white)
//  cimPXMs[im1] is matched to cimPXMs[im2] + virtual warps

double cim_match_images(int im1, int im2)                                  //  v.11.03
{
   uint16      *pix1, vpix2[3];
   int         ww, hh, ww2, hh2;
   int         px1, py1, ii, vstat;
   double      wwi, hhi, ww2i, hh2i, xdisp, ydisp;
   double      wx0, wy0, wx1, wy1, wx2, wy2, wx3, wy3;
   double      dx, dy, px2, py2;
   double      x1, y1, t1, x2, y2, t2;
   double      xoff, yoff, toff, costf, sintf, coeff;
   double      match, cmatch, maxcmatch;
   PXM         *pxm1, *pxm2;
   
   x1 = cimOffs[im1].xf;                                                   //  im1, im2 absolute offsets
   y1 = cimOffs[im1].yf;
   t1 = cimOffs[im1].tf;
   x2 = cimOffs[im2].xf;
   y2 = cimOffs[im2].yf;
   t2 = cimOffs[im2].tf;
   
   xoff = (x2 - x1) * cos(t1) + (y2 - y1) * sin(t1);                       //  offset of im2 relative to im1
   yoff = (y2 - y1) * cos(t1) - (x2 - x1) * sin(t1);
   toff = t2 - t1;

   costf = cos(toff);
   sintf = sin(toff);

   wx0 = cimOffs[im2].wx[0];                                               //  im2 corner warps
   wy0 = cimOffs[im2].wy[0];
   wx1 = cimOffs[im2].wx[1];
   wy1 = cimOffs[im2].wy[1];
   wx2 = cimOffs[im2].wx[2];
   wy2 = cimOffs[im2].wy[2];
   wx3 = cimOffs[im2].wx[3];
   wy3 = cimOffs[im2].wy[3];

   pxm1 = cimPXMs[im1];                                                    //  base image
   pxm2 = cimPXMs[im2];                                                    //  comparison image (virtual warps)

   ww = pxm1->ww;   
   hh = pxm1->hh;
   ww2 = ww / 2;
   hh2 = hh / 2;
   
   wwi = 1.0 / ww;
   hhi = 1.0 / hh;
   ww2i = 1.0 / ww2;
   hh2i = 1.0 / hh2;

   cmatch = 0;
   maxcmatch = 1;
   
   if (cimPano)
   {
      for (py1 = cimOv1ylo; py1 < cimOv1yhi; py1++)                        //  loop overlapping pixels
      for (px1 = cimOv1xlo; px1 < cimOv1xhi; px1++)
      {
         ii = py1 * ww + px1;                                              //  skip low-contrast pixels
         if (! cimRedpix[ii]) continue;

         pix1 = PXMpix(pxm1,px1,py1);                                      //  image1 pixel
         if (! pix1[2]) continue;                                          //  ignore void pixels

         px2 = costf * (px1 - xoff) + sintf * (py1 - yoff);                //  corresponding image2 pixel
         py2 = costf * (py1 - yoff) - sintf * (px1 - xoff);
         
         dx = dy = 0.0;                                                    //  corner warp

         xdisp = (px2 - ww2) * ww2i;                                       //  -1 ... 0 ... +1
         ydisp = (py2 - hh2) * hh2i;                                       //  v.11.04
         
         if (xdisp > 0)                                                    //  right half, no warp
            dx = dy = 0;

         else if (ydisp < 0) {                                             //  use NW corner warp
            dx = wx0 * xdisp * ydisp;
            dy = wy0 * xdisp * ydisp;
         }

         else {                                                            //  use SW corner warp
            dx = wx3 * xdisp * ydisp;
            dy = wy3 * xdisp * ydisp;
         }

         px2 += dx;                                                        //  source pixel location
         py2 += dy;                                                        //    after corner warps
         
         vstat = vpixel(pxm2,px2,py2,vpix2);
         if (! vstat) continue;

         match = cim_match_pixels(pix1,vpix2);                             //  compare brightness adjusted
         cmatch += match;                                                  //  accumulate total match
         maxcmatch += 1.0;
      }
   }

   else if (cimPanoV)
   {
      for (py1 = cimOv1ylo; py1 < cimOv1yhi; py1++)                        //  loop overlapping pixels
      for (px1 = cimOv1xlo; px1 < cimOv1xhi; px1++)
      {
         ii = py1 * ww + px1;                                              //  skip low-contrast pixels
         if (! cimRedpix[ii]) continue;

         pix1 = PXMpix(pxm1,px1,py1);                                      //  image1 pixel
         if (! pix1[2]) continue;                                          //  ignore void pixels

         px2 = costf * (px1 - xoff) + sintf * (py1 - yoff);                //  corresponding image2 pixel
         py2 = costf * (py1 - yoff) - sintf * (px1 - xoff);
         
         dx = dy = 0.0;                                                    //  corner warp

         xdisp = (px2 - ww2) * ww2i;                                       //  -1 ... 0 ... +1
         ydisp = (py2 - hh2) * hh2i;
         
         if (ydisp > 0)                                                    //  bottom half, no warp
            dx = dy = 0;

         else if (xdisp < 0) {                                             //  use NW corner warp
            dx = wx0 * xdisp * ydisp;
            dy = wy0 * xdisp * ydisp;
         }

         else {                                                            //  use NE corner warp
            dx = wx1 * xdisp * ydisp;
            dy = wy1 * xdisp * ydisp;
         }

         px2 += dx;                                                        //  source pixel location
         py2 += dy;                                                        //    after corner warps
         
         vstat = vpixel(pxm2,px2,py2,vpix2);
         if (! vstat) continue;

         match = cim_match_pixels(pix1,vpix2);                             //  compare brightness adjusted
         cmatch += match;                                                  //  accumulate total match
         maxcmatch += 1.0;
      }
   }

   else
   {
      for (py1 = cimOv1ylo; py1 < cimOv1yhi; py1++)                        //  loop overlapping pixels
      for (px1 = cimOv1xlo; px1 < cimOv1xhi; px1++)
      {
         ii = py1 * ww + px1;                                              //  skip low-contrast pixels
         if (! cimRedpix[ii]) continue;

         pix1 = PXMpix(pxm1,px1,py1);                                      //  image1 pixel
         if (! pix1[2]) continue;                                          //  ignore void pixels

         px2 = costf * (px1 - xoff) + sintf * (py1 - yoff);                //  corresponding image2 pixel
         py2 = costf * (py1 - yoff) - sintf * (px1 - xoff);
         
         dx = dy = 0.0;                                                    //  corner warp

         coeff = (1.0 - py2 * hhi - px2 * wwi);                            //  corner 0  NW
         if (coeff > 0) {
            dx += coeff * wx0;
            dy += coeff * wy0;
         }
         coeff = (1.0 - py2 * hhi - (ww - px2) * wwi);                     //  corner 1  NE
         if (coeff > 0) {
            dx += coeff * wx1;
            dy += coeff * wy1;
         }
         coeff = (1.0 - (hh - py2) * hhi - (ww - px2) * wwi);              //  corner 2  SE
         if (coeff > 0) {
            dx += coeff * wx2;
            dy += coeff * wy2;
         }
         coeff = (1.0 - (hh - py2) * hhi - px2 * wwi);                     //  corner 3  SW
         if (coeff > 0) {
            dx += coeff * wx3;
            dy += coeff * wy3;
         }

         px2 += dx;                                                        //  source pixel location
         py2 += dy;                                                        //    after corner warps
         
         vstat = vpixel(pxm2,px2,py2,vpix2);
         if (! vstat) continue;

         match = cim_match_pixels(pix1,vpix2);                             //  compare brightness adjusted
         cmatch += match;                                                  //  accumulate total match
         maxcmatch += 1.0;
      }
   }
   
   return cmatch / maxcmatch;
}


//  combine and show all images
//  fnew >> make new E3 output image and adjust x and y offsets
//  cimPXMw[*] >> E3pxm16 >> main window
//  fblend:  0 > 50/50 blend,  1 > gradual blend

namespace  cim_show_images_names {
   int         im1, im2, iminc, fblendd;
   int         wwlo[10], wwhi[10];
   int         hhlo[10], hhhi[10];
   double      costf[10], sintf[10];
}

void cim_show_images(int fnew, int fblend)                                 //  v.10.7
{
   using namespace cim_show_images_names;

   void * cim_show_images_wthread(void *arg);

   int         imx, pxr, pyr, ii, px3, py3;
   int         ww, hh, wwmin, wwmax, hhmin, hhmax, bmid;
   double      xf, yf, tf;
   uint16      *pix3;
   
   mutex_lock(&Fpixmap_lock);                                              //  stop window updates
   
   fblendd = fblend;                                                       //  blend 50/50 or gradual ramp

   im1 = cimShowIm1;                                                       //  two images to show
   im2 = cimShowIm2;
   iminc = im2 - im1;                                                      //  v.10.9
   
   if (cimShowAll) {                                                       //  show all images     v.10.9
      im1 = 0;
      im2 = cimNF-1;
      iminc = 1;
   }

   for (imx = 0; imx < cimNF; imx++) {                                     //  pre-calculate
      costf[imx] = cos(cimOffs[imx].tf);
      sintf[imx] = sin(cimOffs[imx].tf);
   }

   if (fnew) PXM_free(E3pxm16);                                            //  force new output pixmap
   
   if (! E3pxm16)                                                          //  allocate output pixmap
   {
      wwmin = hhmin = 9999;                                                //  initial values
      wwmax = cimPXMw[im2]->ww;
      hhmax = cimPXMw[im2]->hh;
      
      for (imx = im1; imx <= im2; imx += iminc)                            //  find min and max ww and hh extents
      {
         xf = cimOffs[imx].xf;
         yf = cimOffs[imx].yf;
         tf = cimOffs[imx].tf;
         ww = cimPXMw[imx]->ww;
         hh = cimPXMw[imx]->hh;
         if (xf < wwmin) wwmin = xf;
         if (xf - tf * hh < wwmin) wwmin = xf + tf * hh;
         if (xf + ww > wwmax) wwmax = xf + ww;
         if (xf + ww - tf * hh > wwmax) wwmax = xf + ww - tf * hh;
         if (yf < hhmin) hhmin = yf;
         if (yf + tf * ww < hhmin) hhmin = yf + tf * ww;
         if (yf + hh > hhmax) hhmax = yf + hh;
         if (yf + hh + tf * ww > hhmax) hhmax = yf + hh + tf * ww;
      }

      for (imx = im1; imx <= im2; imx += iminc) {                          //  align to top and left edges
         cimOffs[imx].xf -= wwmin;
         cimOffs[imx].yf -= hhmin;
      }
      wwmax = wwmax - wwmin;
      hhmax = hhmax - hhmin;
      wwmin = hhmin = 0;

      if (cimPano) {
         for (imx = im1; imx <= im2; imx += iminc)                         //  deliberate margins      v.11.03
            cimOffs[imx].yf += 10;
         hhmax += 20;
      }

      if (cimPanoV) {
         for (imx = im1; imx <= im2; imx += iminc)                         //  deliberate margins      v.11.04
            cimOffs[imx].xf += 10;
         wwmax += 20;
      }

      E3pxm16 = PXM_make(wwmax,hhmax,16);                                  //  allocate output image
      E3ww = wwmax;
      E3hh = hhmax;
   }

   for (imx = im1; imx <= im2; imx += iminc)                               //  get ww range of each image
   {
      ww = cimPXMw[imx]->ww;
      hh = cimPXMw[imx]->hh;
      tf = cimOffs[imx].tf;
      wwlo[imx] = cimOffs[imx].xf;
      wwhi[imx] = wwlo[imx] + ww;
      wwlo[imx] -= 0.5 * tf * hh;                                          //  use midpoint of sloping edges
      wwhi[imx] -= 0.5 * tf * hh;
   }

   if (cimBlend) {                                                         //  blend width active
      for (imx = im1; imx <= im2-1; imx += iminc)                          //  reduce for blend width
      {
         if (wwhi[imx] - wwlo[imx+1] > cimBlend) {
            bmid = (wwhi[imx] + wwlo[imx+1]) / 2;
            wwlo[imx+1] = bmid - cimBlend / 2;
            wwhi[imx] = bmid + cimBlend / 2;
         }
      }
   }

   for (ii = 0; ii < Nwt; ii++)                                            //  start worker threads   v.10.7
      start_wthread(cim_show_images_wthread,&wtnx[ii]);
   wait_wthreads();                                                        //  wait for completion

   if (cimRedpix) 
   {
      imx = cimRedImage;                                                   //  paint red pixels for current image
      ww = cimPXMw[imx]->ww;                                               //    being aligned
      hh = cimPXMw[imx]->hh;

      for (ii = 0; ii < ww * hh; ii++)
      {
         if (cimRedpix[ii]) {
            pyr = ii / ww;                                                 //  red pixel
            pxr = ii - pyr * ww;
            px3 = cimOffs[imx].xf + pxr * costf[imx] - pyr * sintf[imx] + 0.5;
            py3 = cimOffs[imx].yf + pyr * costf[imx] + pxr * sintf[imx] + 0.5;
            pix3 = PXMpix(E3pxm16,px3,py3);
            pix3[0] = 65535; pix3[1] = pix3[2] = 1;
         }
      }
   }

   mutex_unlock(&Fpixmap_lock);
   mwpaint2();                                                             //  update window
   zmainloop();                                                            //  v.11.11.1

   return;
}


void * cim_show_images_wthread(void *arg)                                  //  working thread   v.10.7
{
   using namespace cim_show_images_names;

   int         index = *((int *) (arg));
   int         imx, imy;
   int         px3, py3;
   int         vstat, vstat1, vstat2;
   int         red1, green1, blue1;
   int         red2, green2, blue2;
   int         red3, green3, blue3;
   double      f1, f2, px, py;
   uint16      vpix[3], *pix3;
   
   red1 = green1 = blue1 = 0;

   f1 = f2 = 0.5;                                                          //  to use if no fblend flag

   for (py3 = index; py3 < E3hh; py3 += Nwt)                               //  loop E3 rows
   for (px3 = 0; px3 < E3ww; px3++)                                        //  loop E3 columns
   {
      vstat1 = vstat2 = 0;
      
      for (imx = imy = im1; imx <= im2; imx += iminc)                      //  find which images overlap this pixel
      {
         if (px3 < wwlo[imx] || px3 > wwhi[imx]) continue;
         px = costf[imx] * (px3 - cimOffs[imx].xf) + sintf[imx] * (py3 - cimOffs[imx].yf);
         py = costf[imx] * (py3 - cimOffs[imx].yf) - sintf[imx] * (px3 - cimOffs[imx].xf);
         vstat = vpixel(cimPXMw[imx],px,py,vpix);
         if (! vstat) continue;

         if (! vstat1) {                                                   //  first overlapping image
            vstat1 = 1;
            imy = imx;
            red1 = vpix[0];
            green1 = vpix[1];
            blue1 = vpix[2];
         }
         else {                                                            //  second image
            vstat2 = 1;
            red2 = vpix[0];
            green2 = vpix[1];
            blue2 = vpix[2];
            break;
         }
      }
      
      imx = imy;                                                           //  first of 1 or 2 overlapping images

      if (vstat1) {
         if (! vstat2) {
            red3 = red1;                                                   //  use image1 pixel
            green3 = green1;
            blue3 = blue1; 
         }
         else {                                                            //  use blended image1 + image2 pixels
            if (fblendd) {
               f1 = wwhi[imx] - px3;                                       //  gradual blend
               f2 = px3 - wwlo[imx+1];
               f1 = f1 / (f1 + f2);
               f2 = 1.0 - f1;
            }
            red3 = f1 * red1 + f2 * red2 + 0.5;
            green3 = f1 * green1 + f2 * green2 + 0.5;
            blue3 = f1 * blue1 + f2 * blue2 + 0.5;
         }
      }

      else red3 = green3 = blue3 = 0;                                      //  no overlapping image, use black pixel

      pix3 = PXMpix(E3pxm16,px3,py3);                                      //  output pixel
      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


//  version for vertical panorama

void cim_show_Vimages(int fnew, int fblend)                                //  v.11.04
{
   using namespace cim_show_images_names;

   void * cim_show_Vimages_wthread(void *arg);

   int         imx, pxr, pyr, ii, px3, py3;
   int         ww, hh, wwmin, wwmax, hhmin, hhmax, bmid;
   double      xf, yf, tf;
   uint16      *pix3;
   
   mutex_lock(&Fpixmap_lock);                                              //  stop window updates
   
   fblendd = fblend;                                                       //  blend 50/50 or gradual ramp

   im1 = 0;                                                                //  show all images (pano)
   im2 = cimNF-1;

   for (imx = 0; imx < cimNF; imx++) {                                     //  pre-calculate
      costf[imx] = cos(cimOffs[imx].tf);
      sintf[imx] = sin(cimOffs[imx].tf);
   }

   if (fnew) PXM_free(E3pxm16);                                            //  force new output pixmap
   
   if (! E3pxm16)                                                          //  allocate output pixmap
   {
      wwmin = hhmin = 9999;
      wwmax = hhmax = 0;
      
      for (imx = im1; imx <= im2; imx++)                                   //  find min and max ww and hh extents
      {
         xf = cimOffs[imx].xf;
         yf = cimOffs[imx].yf;
         tf = cimOffs[imx].tf;
         ww = cimPXMw[imx]->ww;
         hh = cimPXMw[imx]->hh;
         if (xf < wwmin) wwmin = xf;
         if (xf - tf * hh < wwmin) wwmin = xf + tf * hh;
         if (xf + ww > wwmax) wwmax = xf + ww;
         if (xf + ww - tf * hh > wwmax) wwmax = xf + ww - tf * hh;
         if (yf < hhmin) hhmin = yf;
         if (yf + tf * ww < hhmin) hhmin = yf + tf * ww;
         if (yf + hh > hhmax) hhmax = yf + hh;
         if (yf + hh + tf * ww > hhmax) hhmax = yf + hh + tf * ww;
      }

      for (imx = im1; imx <= im2; imx++) {                                 //  align to top and left edges
         cimOffs[imx].xf -= wwmin;
         cimOffs[imx].yf -= hhmin;
      }
      wwmax = wwmax - wwmin;
      hhmax = hhmax - hhmin;
      wwmin = hhmin = 0;

      for (imx = im1; imx <= im2; imx++)                                   //  deliberate margins
         cimOffs[imx].xf += 10;
      wwmax += 20;

      E3pxm16 = PXM_make(wwmax,hhmax,16);                                  //  allocate output image
      E3ww = wwmax;
      E3hh = hhmax;
   }

   for (imx = im1; imx <= im2; imx++)                                      //  get hh range of each image
   {
      ww = cimPXMw[imx]->ww;
      hh = cimPXMw[imx]->hh;
      tf = cimOffs[imx].tf;
      hhlo[imx] = cimOffs[imx].yf;
      hhhi[imx] = hhlo[imx] + hh;
      hhlo[imx] += 0.5 * tf * ww;                                          //  use midpoint of sloping edges
      hhhi[imx] += 0.5 * tf * ww;
   }

   if (cimBlend) {                                                         //  blend width active
      for (imx = im1; imx <= im2-1; imx++)                                 //  reduce for blend width
      {
         if (hhhi[imx] - hhlo[imx+1] > cimBlend) {
            bmid = (hhhi[imx] + hhlo[imx+1]) / 2;
            hhlo[imx+1] = bmid - cimBlend / 2;
            hhhi[imx] = bmid + cimBlend / 2;
         }
      }
   }

   for (ii = 0; ii < Nwt; ii++)                                            //  start worker threads   v.10.7
      start_wthread(cim_show_Vimages_wthread,&wtnx[ii]);
   wait_wthreads();                                                        //  wait for completion

   if (cimRedpix) 
   {
      imx = cimRedImage;                                                   //  paint red pixels for current image
      ww = cimPXMw[imx]->ww;                                               //    being aligned
      hh = cimPXMw[imx]->hh;

      for (ii = 0; ii < ww * hh; ii++)
      {
         if (cimRedpix[ii]) {
            pyr = ii / ww;                                                 //  red pixel
            pxr = ii - pyr * ww;
            px3 = cimOffs[imx].xf + pxr * costf[imx] - pyr * sintf[imx] + 0.5;
            py3 = cimOffs[imx].yf + pyr * costf[imx] + pxr * sintf[imx] + 0.5;
            pix3 = PXMpix(E3pxm16,px3,py3);
            pix3[0] = 65535; pix3[1] = pix3[2] = 1;
         }
      }
   }

   mutex_unlock(&Fpixmap_lock);
   mwpaint2();                                                             //  update window
   zmainloop();                                                            //  v.11.11.1
   return;
}


void * cim_show_Vimages_wthread(void *arg)                                 //  working thread   v.11.04
{
   using namespace cim_show_images_names;

   int         index = *((int *) (arg));
   int         imx, imy;
   int         px3, py3;
   int         vstat, vstat1, vstat2;
   int         red1, green1, blue1;
   int         red2, green2, blue2;
   int         red3, green3, blue3;
   double      f1, f2, px, py;
   uint16      vpix[3], *pix3;
   
   red1 = green1 = blue1 = 0;

   f1 = f2 = 0.5;                                                          //  to use if no fblend flag

   for (py3 = index; py3 < E3hh; py3 += Nwt)                               //  loop E3 rows
   for (px3 = 0; px3 < E3ww; px3++)                                        //  loop E3 columns
   {
      vstat1 = vstat2 = 0;
      
      for (imx = imy = im1; imx <= im2; imx++)                             //  find which images overlap this pixel
      {
         if (py3 < hhlo[imx] || py3 > hhhi[imx]) continue;
         px = costf[imx] * (px3 - cimOffs[imx].xf) + sintf[imx] * (py3 - cimOffs[imx].yf);
         py = costf[imx] * (py3 - cimOffs[imx].yf) - sintf[imx] * (px3 - cimOffs[imx].xf);
         vstat = vpixel(cimPXMw[imx],px,py,vpix);
         if (! vstat) continue;

         if (! vstat1) {                                                   //  first overlapping image
            vstat1 = 1;
            imy = imx;
            red1 = vpix[0];
            green1 = vpix[1];
            blue1 = vpix[2];
         }
         else {                                                            //  second image
            vstat2 = 1;
            red2 = vpix[0];
            green2 = vpix[1];
            blue2 = vpix[2];
            break;
         }
      }
      
      imx = imy;                                                           //  first of 1 or 2 overlapping images

      if (vstat1) {
         if (! vstat2) {
            red3 = red1;                                                   //  use image1 pixel
            green3 = green1;
            blue3 = blue1; 
         }
         else {                                                            //  use blended image1 + image2 pixels
            if (fblendd) {
               f1 = hhhi[imx] - py3;                                       //  gradual blend
               f2 = py3 - hhlo[imx+1];
               f1 = f1 / (f1 + f2);
               f2 = 1.0 - f1;
            }
            red3 = f1 * red1 + f2 * red2 + 0.5;
            green3 = f1 * green1 + f2 * green2 + 0.5;
            blue3 = f1 * blue1 + f2 * blue2 + 0.5;
         }
      }

      else red3 = green3 = blue3 = 0;                                      //  no overlapping image, use black pixel

      pix3 = PXMpix(E3pxm16,px3,py3);                                      //  output pixel
      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


//  cut-off edges of output image where all input images do not overlap
//  (HDR HDF Stack)

void cim_trim()                                                            //  v.10.9
{
   int      edgex[8] =  {  0, 1,  2, 2,  2, 1,  0, 0 };                    //  4 corners and 4 midpoints of rectangle
   int      edgey[8] =  {  0, 0,  0, 1,  2, 2,  2, 1 };                    //  0 and 2 mark corners, 1 marks midpoints
   int      edgewx[4] = { +1, -1, -1, +1 };
   int      edgewy[4] = { +1, +1, -1, -1 };

   int      imx, ii, jj, ww, hh, px3, py3, px9, py9;
   int      wwmin, wwmax, hhmin, hhmax;
   double   xf, yf, tf, sintf, costf, px, py, wx, wy;
   uint16   *pix3, *pix9;

   wwmin = hhmin = 0;
   wwmax = E3ww;
   hhmax = E3hh;
   
   for (imx = 0; imx < cimNF; imx++)                                       //  loop all images
   {
      ww = cimPXMw[imx]->ww;                                               //  image size
      hh = cimPXMw[imx]->hh;
      xf = cimOffs[imx].xf;                                                //  alignment offsets
      yf = cimOffs[imx].yf;
      tf = cimOffs[imx].tf;
      sintf = sin(tf);
      costf = cos(tf);
      
      for (ii = 0; ii < 8; ii++)                                           //  8 points around image rectangle
      {
         px = ww * edgex[ii] / 2;                                          //  coordinates before warping
         py = hh * edgey[ii] / 2;
         
         if (edgex[ii] != 1 && edgey[ii] != 1) {                           //  if a corner
            jj = ii / 2;
            wx = cimOffs[imx].wx[jj];                                      //  corner warp
            wy = cimOffs[imx].wy[jj];
            if (edgewx[jj] > 0 && wx < 0) px -= wx;                        //  if warp direction inwards,
            if (edgewx[jj] < 0 && wx > 0) px -= wx;                        //    reduce px/py by warp
            if (edgewy[jj] > 0 && wy < 0) py -= wy;
            if (edgewy[jj] < 0 && wy > 0) py -= wy;
         }

         px3 = xf + px * costf - py * sintf;                               //  map px/py to output image px3/py3
         py3 = yf + py * costf + px * sintf;
         
         if (edgex[ii] != 1) {
            if (px3 < ww/2 && px3 > wwmin) wwmin = px3;                    //  remember px3/py3 extremes
            if (px3 > ww/2 && px3 < wwmax) wwmax = px3;
         }

         if (edgey[ii] != 1) {
            if (py3 < hh/2 && py3 > hhmin) hhmin = py3;
            if (py3 > hh/2 && py3 < hhmax) hhmax = py3;
         }
      }
   }
   
   wwmin += 2;                                                             //  compensate rounding
   wwmax -= 2;
   hhmin += 2;
   hhmax -= 2;

   ww = wwmax - wwmin;                                                     //  new image size
   hh = hhmax - hhmin;

   if (ww < 0.7 * E3ww) return;                                            //  sanity check
   if (hh < 0.7 * E3hh) return;
   
   E9pxm16 = PXM_make(ww,hh,16);

   for (py3 = hhmin; py3 < hhmax; py3++)                                   //  E9 = trimmed E3
   for (px3 = wwmin; px3 < wwmax; px3++)
   {
      px9 = px3 - wwmin;
      py9 = py3 - hhmin;
      pix3 = PXMpix(E3pxm16,px3,py3);
      pix9 = PXMpix(E9pxm16,px9,py9);
      pix9[0] = pix3[0];
      pix9[1] = pix3[1];
      pix9[2] = pix3[2];
   }

   PXM_free(E3pxm16);                                                      //  E3 = E9
   E3pxm16 = E9pxm16;
   E9pxm16 = 0;
   E3ww = ww;
   E3hh = hh;

   return;
}


//  dump offsets to stdout - diagnostic tool

void cim_dump_offsets(cchar *text)
{
   printf("\n offsets: %s \n",text);

   for (int imx = 0; imx < cimNF; imx++)
   {
      printf(" imx %d  x/y/t: %.1f %.1f %.4f  w0: %.1f %.1f  w1: %.1f %.1f  w2: %.1f %.1f  w3: %.1f %.1f \n",
          imx, cimOffs[imx].xf, cimOffs[imx].yf, cimOffs[imx].tf,
               cimOffs[imx].wx[0], cimOffs[imx].wy[0], cimOffs[imx].wx[1], cimOffs[imx].wy[1], 
               cimOffs[imx].wx[2], cimOffs[imx].wy[2], cimOffs[imx].wx[3], cimOffs[imx].wy[3]);
   }

   return;
}


/**************************************************************************

   Make an HDR (high dynamic range) image from several images of the same
   subject with different exposure levels. The composite image has better
   visibility of detail in both the brightest and darkest areas.

***************************************************************************/

int      HDRstat;                                                          //  1 = OK, 0 = failed or canceled
double   HDRinitAlignSize = 160;                                           //  initial align image size
double   HDRimageIncrease = 1.6;                                           //  image size increase per align cycle
double   HDRsampSize = 6000;                                               //  pixel sample size   11.03

double   HDRinitSearchRange = 8.0;                                         //  initial search range, +/- pixels
double   HDRinitSearchStep = 1.0;                                          //  initial search step, pixels 
double   HDRinitWarpRange = 3.0;                                           //  initial corner warp range, +/- pixels
double   HDRinitWarpStep = 0.67;                                           //  initial corner warp step, pixels 
double   HDRsearchRange = 2.0;                                             //  normal search range, +/- pixels
double   HDRsearchStep = 0.67;                                             //  normal search step, pixels 
double   HDRwarpRange = 2.0;                                               //  normal corner warp range, +/- pixels
double   HDRwarpStep = 0.67;                                               //  normal corner warp step, pixels

float    *HDRbright = 0;                                                   //  maps brightness per pixel
zdialog  *HDRzd = 0;                                                       //  tweak dialog
double   HDR_respfac[10][1000];                                            //  contribution / image / pixel brightness

void * HDR_align_thread(void *);                                           //  align 2 images
void   HDR_brightness();                                                   //  compute pixel brightness levels
void   HDR_tweak();                                                        //  adjust image contribution curves
void * HDR_combine_thread(void *);                                         //  combine images per contribution curves

editfunc    EFhdr;                                                         //  edit function data


//  menu function

void m_HDR(GtkWidget *, cchar *)                                           //  v.10.7
{
   char        **flist, *ftemp;
   int         imx, jj, err, px, py, ww, hh;
   double      diffw, diffh;
   double      fbright[10], btemp;
   double      pixsum, fnorm = 3.0 / 65536.0;
   uint16      *pixel;
   PXM         *pxmtemp;
   
   zfuncs::F1_help_topic = "HDR";                                          //  help topic

   if (mod_keep()) return;                                                 //  warn unsaved changes
   if (! menulock(1)) return;                                              //  test menu lock            v.11.07
   menulock(0);

   for (imx = 0; imx < 10; imx++)
   {                                                                       //  clear all file and PXM data
      cimFile[imx] = 0;
      cimPXMf[imx] = cimPXMs[imx] = cimPXMw[imx] = 0;
   }

   cimNF = 0;   
   HDRbright = 0;

   flist = zgetfileN(ZTX("Select 2 to 9 files"),"openN",curr_file);        //  select images to combine
   if (! flist) return;

   for (imx = 0; flist[imx]; imx++);                                       //  count selected files
   if (imx < 2 || imx > 9) {
      zmessageACK(mWin,ZTX("Select 2 to 9 files"));
      goto cleanup;
   }

   cimNF = imx;                                                            //  file count
   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = strdupz(flist[imx],0,"HDR");                          //  set up file list
   
   if (! cim_load_files()) goto cleanup;                                   //  load and check all files

   ww = cimPXMf[0]->ww;
   hh = cimPXMf[0]->hh;
   
   for (imx = 1; imx < cimNF; imx++)                                       //  check image compatibility
   {
      diffw = abs(ww - cimPXMf[imx]->ww);
      diffw = diffw / ww;
      diffh = abs(hh - cimPXMf[imx]->hh);
      diffh = diffh / hh;

      if (diffw > 0.02 || diffh > 0.02) {
         zmessageACK(mWin,ZTX("Images are not all the same size"));
         goto cleanup;
      }
   }
   
   free_resources();                                                       //  ready to commit

   err = f_open(cimFile[0],0);                                             //  curr_file = 1st file in list
   if (err) goto cleanup;

   EFhdr.funcname = "HDR";
   if (! edit_setup(EFhdr)) goto cleanup;                                  //  setup edit (will lock)

   for (imx = 0; imx < cimNF; imx++)                                       //  compute image brightness levels
   {
      pixsum = 0;
      for (py = 0; py < Fhh; py++)
      for (px = 0; px < Fww; px++)
      {
         pixel = PXMpix(cimPXMf[imx],px,py);
         pixsum += fnorm * (pixel[0] + pixel[1] + pixel[2]);
      }
      fbright[imx] = pixsum / (Fww * Fhh);
   }

   for (imx = 0; imx < cimNF; imx++)                                       //  sort file and pixmap lists
   for (jj = imx+1; jj < cimNF; jj++)                                      //    by decreasing brightness
   {
      if (fbright[jj] > fbright[imx]) {                                    //  bubble sort
         btemp = fbright[jj];
         fbright[jj] = fbright[imx];
         fbright[imx] = btemp;
         ftemp = cimFile[jj];
         cimFile[jj] = cimFile[imx];
         cimFile[imx] = ftemp;
         pxmtemp = cimPXMf[jj];
         cimPXMf[jj] = cimPXMf[imx];
         cimPXMf[imx] = pxmtemp;
      }
   }
   
   start_thread(HDR_align_thread,0);                                       //  align each pair of images
   wrapup_thread(0);                                                       //  wait for completion
   if (HDRstat != 1) goto cancel;

   HDR_brightness();                                                       //  compute pixel brightness levels
   if (HDRstat != 1) goto cancel;
   
   HDR_tweak();                                                            //  combine images based on user inputs
   if (HDRstat != 1) goto cancel;

   CEF->Fmod = 1;                                                          //  done
   edit_done(EFhdr);
   goto cleanup;

cancel:
   edit_cancel(EFhdr);

cleanup:

   if (flist) {
      for (imx = 0; flist[imx]; imx++)                                     //  free file list
         zfree(flist[imx]);
      zfree(flist);
   }

   for (imx = 0; imx < cimNF; imx++) {                                     //  free cim file and PXM data
      if (cimFile[imx]) zfree(cimFile[imx]);
      if (cimPXMf[imx]) PXM_free(cimPXMf[imx]);
      if (cimPXMs[imx]) PXM_free(cimPXMs[imx]);
      if (cimPXMw[imx]) PXM_free(cimPXMw[imx]);
   }
   
   if (HDRbright) zfree(HDRbright);
   *SB_text = 0;

   return;
}


//  HDR align each pair of input images, output combined image to E3pxm16
//  cimPXMf[*]  original image
//  cimPXMs[*]  scaled and color adjusted for pixel comparisons
//  cimPXMw[*]  warped for display

void * HDR_align_thread(void *)                                            //  v.10.7
{
   int         imx, im1, im2, ww, hh, ii, nn;
   double      R, maxtf, mintf, midtf;
   double      xoff, yoff, toff, dxoff, dyoff;
   cimoffs     offsets[10];                                                //  x/y/t offsets after alignment
   
   Fzoom = 0;                                                              //  fit to window if big
   Fblowup = 1;                                                            //  scale up to window if small
   Ffuncbusy++;                                                            //  v.11.01
   cimShrink = 0;                                                          //  no warp shrinkage (pano)
   cimPano = cimPanoV = 0;                                                 //  no pano mode

   for (imx = 0; imx < cimNF; imx++)                                       //  bugfix     v.10.8
      memset(&offsets[imx],0,sizeof(cimoffs));

   for (im1 = 0; im1 < cimNF-1; im1++)                                     //  loop each pair of images
   {
      im2 = im1 + 1;

      memset(&cimOffs[im1],0,sizeof(cimoffs));                             //  initial image offsets = 0
      memset(&cimOffs[im2],0,sizeof(cimoffs));

      ww = cimPXMf[im1]->ww;                                               //  image dimensions
      hh = cimPXMf[im1]->hh;

      nn = ww;                                                             //  use larger of ww, hh
      if (hh > ww) nn = hh;
      cimScale = HDRinitAlignSize / nn;                                    //  initial align image size
      if (cimScale > 1.0) cimScale = 1.0;

      cimBlend = 0;                                                        //  no blend width (use all)
      cim_get_overlap(im1,im2,cimPXMf);                                    //  get overlap area
      cim_match_colors(im1,im2,cimPXMf);                                   //  get color matching factors

      cimSearchRange = HDRinitSearchRange;                                 //  initial align search range
      cimSearchStep = HDRinitSearchStep;                                   //  initial align search step
      cimWarpRange = HDRinitWarpRange;                                     //  initial align corner warp range
      cimWarpStep = HDRinitWarpStep;                                       //  initial align corner warp step
      cimSampSize = HDRsampSize;                                           //  pixel sample size for align/compare
      cimNsearch = 0;                                                      //  reset align search counter

      while (true)                                                         //  loop, increasing image size
      {
         cim_scale_image(im1,cimPXMs);                                     //  scale images to cimScale
         cim_scale_image(im2,cimPXMs);

         cim_adjust_colors(cimPXMs[im1],1);                                //  apply color adjustments
         cim_adjust_colors(cimPXMs[im2],2);

         cim_warp_image(im1);                                              //  make warped images to show
         cim_warp_image(im2);      

         cimShowIm1 = im1;                                                 //  show two images with 50/50 blend
         cimShowIm2 = im2;
         cimShowAll = 0;
         cim_show_images(1,0);                                             //  (x/y offsets can change)

         cim_get_overlap(im1,im2,cimPXMs);                                 //  get overlap area             v.11.04
         cim_get_redpix(im1);                                              //  get high-contrast pixels

         cim_align_image(im1,im2);                                         //  align im2 to im1

         zfree(cimRedpix);                                                 //  clear red pixels
         cimRedpix = 0;

         if (cimScale == 1.0) break;                                       //  done

         R = HDRimageIncrease;                                             //  next larger image size
         cimScale = cimScale * R;
         if (cimScale > 0.85) {                                            //  if close to end, jump to end
            R = R / cimScale;
            cimScale = 1.0;
         }

         cimOffs[im1].xf *= R;                                             //  scale offsets for larger image
         cimOffs[im1].yf *= R;
         cimOffs[im2].xf *= R;
         cimOffs[im2].yf *= R;

         for (ii = 0; ii < 4; ii++) {
            cimOffs[im1].wx[ii] *= R;
            cimOffs[im1].wy[ii] *= R;
            cimOffs[im2].wx[ii] *= R;
            cimOffs[im2].wy[ii] *= R;
         }

         cimSearchRange = HDRsearchRange;                                  //  align search range
         cimSearchStep = HDRsearchStep;                                    //  align search step size
         cimWarpRange = HDRwarpRange;                                      //  align corner warp range
         cimWarpStep = HDRwarpStep;                                        //  align corner warp step size
      }

      offsets[im2].xf = cimOffs[im2].xf - cimOffs[im1].xf;                 //  save im2 offsets from im1
      offsets[im2].yf = cimOffs[im2].yf - cimOffs[im1].yf;
      offsets[im2].tf = cimOffs[im2].tf - cimOffs[im1].tf;
      
      for (ii = 0; ii < 4; ii++) {
         offsets[im2].wx[ii] = cimOffs[im2].wx[ii] - cimOffs[im1].wx[ii];
         offsets[im2].wy[ii] = cimOffs[im2].wy[ii] - cimOffs[im1].wy[ii];
      }
   }
   
   for (imx = 0; imx < cimNF; imx++)                                       //  offsets[*] >> cimOffs[*]
      cimOffs[imx] = offsets[imx];

   cimOffs[0].xf = cimOffs[0].yf = cimOffs[0].tf = 0;                      //  image 0 at (0,0,0)

   for (im1 = 0; im1 < cimNF-1; im1++)                                     //  absolute offsets for image 1 to last
   {
      im2 = im1 + 1;
      cimOffs[im2].xf += cimOffs[im1].xf;                                  //  x/y/t offsets are additive
      cimOffs[im2].yf += cimOffs[im1].yf;
      cimOffs[im2].tf += cimOffs[im1].tf;

      for (ii = 0; ii < 4; ii++) {                                         //  corner warps are additive
         cimOffs[im2].wx[ii] += cimOffs[im1].wx[ii];
         cimOffs[im2].wy[ii] += cimOffs[im1].wy[ii];
      }
   }

   for (imx = 1; imx < cimNF; imx++)                                       //  re-warp to absolute       v.10.8
      cim_warp_image(imx);

   toff = cimOffs[0].tf;                                                   //  balance +/- thetas
   maxtf = mintf = toff;
   for (imx = 1; imx < cimNF; imx++) {
      toff = cimOffs[imx].tf;
      if (toff > maxtf) maxtf = toff;
      if (toff < mintf) mintf = toff;
   }
   midtf = 0.5 * (maxtf + mintf);
   
   for (imx = 0; imx < cimNF; imx++)
      cimOffs[imx].tf -= midtf;

   for (im1 = 0; im1 < cimNF-1; im1++)                                     //  adjust x/y offsets for images after im1
   for (im2 = im1+1; im2 < cimNF; im2++)                                   //    due to im1 theta offset
   {
      toff = cimOffs[im1].tf;
      xoff = cimOffs[im2].xf - cimOffs[im1].xf;
      yoff = cimOffs[im2].yf - cimOffs[im1].yf;
      dxoff = yoff * sin(toff);
      dyoff = xoff * sin(toff);
      cimOffs[im2].xf -= dxoff;
      cimOffs[im2].yf += dyoff;
   }

   Fzoom = Fblowup = 0;
   Ffuncbusy--;
   HDRstat = 1;
   thread_exit();
   return 0;                                                               //  not executed
}


//  Compute mean image pixel brightness levels.
//  (basis for setting image contributions per brightness level)

void HDR_brightness()                                                      //  v.10.7
{
   int         px3, py3, ww, hh, imx, kk, vstat;
   double      px, py, red, green, blue;
   double      bright, maxbright, minbright;
   double      xoff, yoff, sintf[10], costf[10];
   double      norm, fnorm = 1.0 / 65536.0;
   uint16      vpix[3], *pix3;

   cimScale = 1.0;

   for (imx = 0; imx < cimNF; imx++)                                       //  replace alignment images
   {                                                                       //   (color adjusted for pixel matching)
      PXM_free(cimPXMs[imx]);                                              //    with the original images
      cimPXMs[imx] = PXM_copy(cimPXMf[imx]);
      cim_warp_image(imx);                                                 //  re-apply warps
   }

   for (imx = 0; imx < cimNF; imx++)                                       //  pre-calculate trig functions
   {
      sintf[imx] = sin(cimOffs[imx].tf);
      costf[imx] = cos(cimOffs[imx].tf);
   }
   
   ww = E3pxm16->ww;
   hh = E3pxm16->hh;

   HDRbright = (float *) zmalloc(ww*hh*sizeof(int),"HDR");                 //  get memory for brightness array
   
   minbright = 1.0;
   maxbright = 0.0;
   
   for (py3 = 0; py3 < hh; py3++)                                          //  step through all output pixels
   for (px3 = 0; px3 < ww; px3++)
   {
      red = green = blue = 0;
      vstat = 0;

      for (imx = 0; imx < cimNF; imx++)                                    //  step through all input images
      {
         xoff = cimOffs[imx].xf;
         yoff = cimOffs[imx].yf;

         px = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);       //  image N pixel, after offsets
         py = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);
         vstat = vpixel(cimPXMw[imx],px,py,vpix);
         if (! vstat) break;
         
         red += fnorm * vpix[0];                                           //  sum input pixels
         green += fnorm * vpix[1];
         blue += fnorm * vpix[2];
      }
      
      if (! vstat) {                                                       //  pixel outside some image
         pix3 = PXMpix(E3pxm16,px3,py3);                                   //  output pixel = black
         pix3[0] = pix3[1] = pix3[2] = 0;
         kk = py3 * ww + px3;
         HDRbright[kk] = 0;
         continue;
      }
      
      bright = (red + green + blue) / (3 * cimNF);                         //  mean pixel brightness, 0.0 to 1.0
      kk = py3 * ww + px3;
      HDRbright[kk] = bright;
      
      if (bright > maxbright) maxbright = bright;
      if (bright < minbright) minbright = bright;

      pix3 = PXMpix(E3pxm16,px3,py3);                                      //  output pixel
      pix3[0] = red * 65535.0 / cimNF;
      pix3[1] = green * 65535.0 / cimNF;
      pix3[2] = blue * 65535.0 / cimNF;
   }
   
   norm = 0.999 / (maxbright - minbright);                                 //  normalize to range 0.0 to 0.999   

   for (int ii = 0; ii < ww * hh; ii++)
      HDRbright[ii] = (HDRbright[ii] - minbright) * norm;

   mwpaint2();                                                             //  update window
   return;
}


//  Dialog for user to control the contributions of each input image
//  while watching the output image which is updated in real time.

void HDR_tweak()                                                           //  v.10.7
{
   int    HDR_tweak_event(zdialog *zd, cchar *event);
   void   HDR_curvedit(int);

   int         imx;
   double      cww = 1.0 / (cimNF-1);
   
   HDRzd = zdialog_new(ZTX("Adjust Image Contributions"),mWin,Bdone,Bcancel,null);
   zdialog_add_widget(HDRzd,"frame","brframe","dialog",0,"expand|space=2");
   zdialog_add_widget(HDRzd,"hbox","hb1","dialog",0);
   zdialog_add_widget(HDRzd,"label","lab11","hb1",ZTX("dark pixels"),"space=3");
   zdialog_add_widget(HDRzd,"label","lab12","hb1",0,"expand");
   zdialog_add_widget(HDRzd,"label","lab13","hb1",ZTX("light pixels"),"space=3");
   zdialog_add_widget(HDRzd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(HDRzd,"label","labf1","hb2",ZTX("file:"),"space=3");
   zdialog_add_widget(HDRzd,"label","labf2","hb2","*");

   zdialog_add_widget(HDRzd,"hbox","hbcf","dialog",0,"space=5");
   zdialog_add_widget(HDRzd,"label","labcf","hbcf",Bcurvefile,"space=5");
   zdialog_add_widget(HDRzd,"button","load","hbcf",Bopen,"space=5");
   zdialog_add_widget(HDRzd,"button","save","hbcf",Bsave,"space=5");

   GtkWidget *brframe = zdialog_widget(HDRzd,"brframe");                   //  set up curve edit
   spldat *sd = splcurve_init(brframe,HDR_curvedit);                       //  v.11.01
   EFhdr.curves = sd;

   sd->Nspc = cimNF;                                                       //  no. curves = no. files
   
   for (imx = 0; imx < cimNF; imx++)                                       //  set up initial response curve
   {                                                                       //    anchor points
      sd->vert[imx] = 0;
      sd->nap[imx] = 2;
      sd->apx[imx][0] = 0.01;                                              //  flatter curves, v.9.3
      sd->apx[imx][1] = 0.99;
      sd->apy[imx][0] = 0.9 - imx * 0.8 * cww;
      sd->apy[imx][1] = 0.1 + imx * 0.8 * cww;
      splcurve_generate(sd,imx);
   }
   
   start_thread(HDR_combine_thread,0);                                     //  start working thread
   signal_thread();

   zdialog_resize(HDRzd,400,360);
   zdialog_run(HDRzd,HDR_tweak_event,"-10/20");                            //  run dialog             v.11.07
   zdialog_wait(HDRzd);                                                    //  wait for completion

   return;
}


//  dialog event and completion callback function

int HDR_tweak_event(zdialog *zd, cchar *event)
{
   spldat *sd = EFhdr.curves;

   if (strEqu(event,"load")) {                                             //  load saved curve       v.11.02
      splcurve_load(sd);
      zdialog_stuff(HDRzd,"labf2","*");
      signal_thread();
      return 0;
   }

   if (strEqu(event,"save")) {                                             //  save curve to file     v.11.02
      splcurve_save(sd);
      return 0;
   }

   if (zd->zstat)                                                          //  dialog complete
   {
      wrapup_thread(8);
      if (zd->zstat == 1) HDRstat = 1;
      else HDRstat = 0;
      zdialog_free(HDRzd);
      if (HDRstat == 1) cim_trim();                                        //  cut-off edges       v.10.9
   }
   
   return 1;
}


//  this function is called when a curve is edited

void HDR_curvedit(int spc)
{
   cchar  *pp; 
   
   pp = strrchr(cimFile[spc],'/');
   zdialog_stuff(HDRzd,"labf2",pp+1);
   signal_thread();
   return;
}


//  Combine all input images >> E3pxm16 based on image response curves.

void * HDR_combine_thread(void *)
{
   void * HDR_combine_wthread(void *arg);

   int         imx, ii, kk;
   double      xlo, xhi, xval, yval, sumrf;
   spldat      *sd = EFhdr.curves;

   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request
      
      for (imx = 0; imx < cimNF; imx++)                                    //  loop input images
      {
         ii = sd->nap[imx];                                                //  get low and high anchor points
         xlo = sd->apx[imx][0];                                            //    for image response curve
         xhi = sd->apx[imx][ii-1];
         if (xlo < 0.02) xlo = 0;                                          //  snap-to scale end points
         if (xhi > 0.98) xhi = 1;

         for (ii = 0; ii < 1000; ii++)                                     //  loop all brightness levels
         {
            HDR_respfac[imx][ii] = 0;
            xval = 0.001 * ii;
            if (xval < xlo || xval > xhi) continue;                        //  no influence for brightness level
            kk = 1000 * xval;                                              //  speedup    v.11.06
            yval = sd->yval[imx][kk];
            HDR_respfac[imx][ii] = yval;                                   //    = contribution of this input image
         }
      }

      for (ii = 0; ii < 1000; ii++)                                        //  normalize the factors so that
      {                                                                    //    they sum to 1.0
         sumrf = 0;
         for (imx = 0; imx < cimNF; imx++)
            sumrf += HDR_respfac[imx][ii];
         if (! sumrf) continue;
         for (imx = 0; imx < cimNF; imx++)
            HDR_respfac[imx][ii] = HDR_respfac[imx][ii] / sumrf;
      }

      mutex_lock(&Fpixmap_lock);                                           //  stop window updates

      for (ii = 0; ii < Nwt; ii++)                                         //  start worker threads      v.10.7
         start_wthread(HDR_combine_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion
   
      mutex_unlock(&Fpixmap_lock);
      mwpaint2();                                                          //  update window
   }

   return 0;                                                               //  not executed
}


void * HDR_combine_wthread(void *arg)                                      //  working thread
{
   int         index = *((int *) (arg));
   int         imx, ww, hh, ii, px3, py3, vstat;
   double      sintf[10], costf[10], xoff, yoff;
   double      px, py, red, green, blue, bright, factor;
   uint16      vpix[3], *pix3;

   for (imx = 0; imx < cimNF; imx++)                                       //  pre-calculate trig functions
   {
      sintf[imx] = sin(cimOffs[imx].tf);
      costf[imx] = cos(cimOffs[imx].tf);
   }

   ww = E3pxm16->ww;
   hh = E3pxm16->hh;

   for (py3 = index; py3 < hh; py3 += Nwt)                                 //  step through all output pixels
   for (px3 = 0; px3 < ww; px3++)
   {
      ii = py3 * ww + px3;
      bright = HDRbright[ii];                                              //  mean brightness, 0.0 to 1.0
      ii = 1000 * bright;
      
      red = green = blue = 0;
      
      for (imx = 0; imx < cimNF; imx++)                                    //  loop input images
      {
         factor = HDR_respfac[imx][ii];                                    //  image contribution to this pixel
         if (! factor) continue;                                           //  none
         
         xoff = cimOffs[imx].xf;
         yoff = cimOffs[imx].yf;

         px = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);       //  input virtual pixel mapping to
         py = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);       //    this output pixel

         vstat = vpixel(cimPXMw[imx],px,py,vpix);                          //  get input pixel
         if (! vstat) continue;

         red += factor * vpix[0];                                          //  accumulate brightness contribution
         green += factor * vpix[1];
         blue += factor * vpix[2];
      }
         
      pix3 = PXMpix(E3pxm16,px3,py3);                                      //  output pixel

      pix3[0] = red;                                                       //  = sum of input pixel contributions
      pix3[1] = green;
      pix3[2] = blue;
   }
   
   exit_wthread();
   return 0;                                                               //  not executed
}


/**************************************************************************

   Make an HDF (high depth of field) image from several images of the same
   subject with different focus settings. Combine the images and allow the
   user to "paint" the output composite image using the mouse and choosing 
   the sharpest input image for each area of the output image. The result 
   is an image with a depth of field that exceeds the camera capability.
   
   The images are aligned at the center, but small differences in camera 
   position (hand-held photos) will cause parallax errors that prevent 
   perfect alignment of the images. Also, the images with nearer focus
   will be slightly larger than those with farther focus. These problems
   can be compensated by dragging and warping the images using the mouse.                 v.10.7 

**************************************************************************/

int      HDFstat;                                                          //  1 = OK, 0 = failed or canceled
double   HDFinitAlignSize = 160;                                           //  initial align image size
double   HDFimageIncrease = 1.6;                                           //  image size increase per align cycle
double   HDFsampSize = 6000;                                               //  pixel sample size

double   HDFinitSearchRange = 8.0;                                         //  initial search range, +/- pixels
double   HDFinitSearchStep = 1.0;                                          //  initial search step, pixels
double   HDFinitWarpRange = 4.0;                                           //  initial corner warp range
double   HDFinitWarpStep = 1.0;                                            //  initial corner warp step
double   HDFsearchRange = 2.0;                                             //  normal search range
double   HDFsearchStep = 1.0;                                              //  normal search step
double   HDFwarpRange = 1.0;                                               //  normal corner warp range     v.11.03
double   HDFwarpStep = 0.67;                                               //  normal corner warp step

void * HDF_align_thread(void *);
void   HDF_tweak();
void   HDF_mousefunc();
void * HDF_combine_thread(void *);

editfunc    EFhdf;                                                         //  edit function data


//  menu function

void m_HDF(GtkWidget *, cchar *)                                           //  v.10.7
{
   char        **flist;
   int         imx, err, ww, hh;
   double      diffw, diffh;
   
   zfuncs::F1_help_topic = "HDF";                                          //  help topic

   if (mod_keep()) return;                                                 //  warn unsaved changes
   if (! menulock(1)) return;                                              //  test menu lock            v.11.07
   menulock(0);

   for (imx = 0; imx < 10; imx++)
   {                                                                       //  clear all file and PXM data
      cimFile[imx] = 0;
      cimPXMf[imx] = cimPXMs[imx] = cimPXMw[imx] = 0;
   }

   cimNF = 0;   

   flist = zgetfileN(ZTX("Select 2 to 9 files"),"openN",curr_file);        //  select images to combine
   if (! flist) return;

   for (imx = 0; flist[imx]; imx++);                                       //  count selected files
   if (imx < 2 || imx > 9) {
      zmessageACK(mWin,ZTX("Select 2 to 9 files"));
      goto cleanup;
   }

   cimNF = imx;                                                            //  file count
   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = strdupz(flist[imx],0,"HDF");                          //  set up file list
   
   if (! cim_load_files()) goto cleanup;                                   //  load and check all files

   ww = cimPXMf[0]->ww;
   hh = cimPXMf[0]->hh;
   
   for (imx = 1; imx < cimNF; imx++)                                       //  check image compatibility
   {
      diffw = abs(ww - cimPXMf[imx]->ww);
      diffw = diffw / ww;
      diffh = abs(hh - cimPXMf[imx]->hh);
      diffh = diffh / hh;

      if (diffw > 0.02 || diffh > 0.02) {
         zmessageACK(mWin,ZTX("Images are not all the same size"));
         goto cleanup;
      }
   }

   free_resources();                                                       //  ready to commit

   err = f_open(cimFile[0],0);                                             //  curr_file = 1st file in list
   if (err) goto cleanup;

   EFhdf.funcname = "HDF";
   if (! edit_setup(EFhdf)) goto cleanup;                                  //  setup edit (will lock)

   start_thread(HDF_align_thread,0);                                       //  align each pair of images
   wrapup_thread(0);                                                       //  wait for completion
   if (HDFstat != 1) goto cancel;

   HDF_tweak();                                                            //  combine images based on user inputs
   if (HDFstat != 1) goto cancel;

   CEF->Fmod = 1;                                                          //  done
   edit_done(EFhdf);
   goto cleanup;

cancel:
   edit_cancel(EFhdf);

cleanup:

   if (flist) {
      for (imx = 0; flist[imx]; imx++)                                     //  free file list
         zfree(flist[imx]);
      zfree(flist);
   }

   for (imx = 0; imx < cimNF; imx++) {                                     //  free cim file and PXM data
      if (cimFile[imx]) zfree(cimFile[imx]);
      if (cimPXMf[imx]) PXM_free(cimPXMf[imx]);
      if (cimPXMs[imx]) PXM_free(cimPXMs[imx]);
      if (cimPXMw[imx]) PXM_free(cimPXMw[imx]);
   }
   
   *SB_text = 0;
   return;
}


//  HDF align each pair of input images, output combined image to E3pxm16
//  cimPXMf[*]  original image
//  cimPXMs[*]  scaled and color adjusted for pixel comparisons
//  cimPXMw[*]  warped for display

void * HDF_align_thread(void *)                                            //  v.10.7
{
   int         imx, im1, im2, ww, hh, ii, nn;
   double      R, maxtf, mintf, midtf;
   double      xoff, yoff, toff, dxoff, dyoff;
   cimoffs     offsets[10];                                                //  x/y/t offsets after alignment
   
   Fzoom = 0;                                                              //  fit to window if big
   Fblowup = 1;                                                            //  scale up to window if small
   Ffuncbusy++;                                                            //  v.11.01
   cimShrink = 0;                                                          //  no warp shrinkage (pano)
   cimPano = cimPanoV = 0;                                                 //  no pano mode

   for (imx = 0; imx < cimNF; imx++)                                       //  bugfix     v.10.8
      memset(&offsets[imx],0,sizeof(cimoffs));
   
   for (im1 = 0; im1 < cimNF-1; im1++)                                     //  loop each pair of images
   {
      im2 = im1 + 1;

      memset(&cimOffs[im1],0,sizeof(cimoffs));                             //  initial image offsets = 0
      memset(&cimOffs[im2],0,sizeof(cimoffs));
      
      ww = cimPXMf[im1]->ww;                                               //  image dimensions
      hh = cimPXMf[im1]->hh;

      nn = ww;                                                             //  use larger of ww, hh
      if (hh > ww) nn = hh;
      cimScale = HDFinitAlignSize / nn;                                    //  initial align image size
      if (cimScale > 1.0) cimScale = 1.0;

      cimBlend = 0;                                                        //  no blend width (use all)
      cim_get_overlap(im1,im2,cimPXMf);                                    //  get overlap area
      cim_match_colors(im1,im2,cimPXMf);                                   //  get color matching factors

      cimSearchRange = HDFinitSearchRange;                                 //  initial align search range
      cimSearchStep = HDFinitSearchStep;                                   //  initial align search step
      cimWarpRange = HDFinitWarpRange;                                     //  initial align corner warp range
      cimWarpStep = HDFinitWarpStep;                                       //  initial align corner warp step
      cimSampSize = HDFsampSize;                                           //  pixel sample size for align/compare
      cimNsearch = 0;                                                      //  reset align search counter

      while (true)                                                         //  loop, increasing image size
      {
         cim_scale_image(im1,cimPXMs);                                     //  scale images to cimScale
         cim_scale_image(im2,cimPXMs);

         cim_adjust_colors(cimPXMs[im1],1);                                //  apply color adjustments
         cim_adjust_colors(cimPXMs[im2],2);

         cim_warp_image(im1);                                              //  warp images for show
         cim_warp_image(im2);

         cimShowIm1 = im1;                                                 //  show these two images
         cimShowIm2 = im2;                                                 //    with 50/50 blend
         cimShowAll = 0;
         cim_show_images(1,0);                                             //  (y offset can change)

         cim_get_overlap(im1,im2,cimPXMs);                                 //  get overlap area             v.11.04
         cim_get_redpix(im1);                                              //  get high-contrast pixels

         cim_align_image(im1,im2);                                         //  align im2 to im1
         
         zfree(cimRedpix);                                                 //  clear red pixels
         cimRedpix = 0;

         if (cimScale == 1.0) break;                                       //  done

         R = HDFimageIncrease;                                             //  next larger image size
         cimScale = cimScale * R;
         if (cimScale > 0.85) {                                            //  if close to end, jump to end
            R = R / cimScale;
            cimScale = 1.0;
         }

         cimOffs[im1].xf *= R;                                             //  scale offsets for larger image
         cimOffs[im1].yf *= R;
         cimOffs[im2].xf *= R;
         cimOffs[im2].yf *= R;

         for (ii = 0; ii < 4; ii++) {
            cimOffs[im1].wx[ii] *= R;
            cimOffs[im1].wy[ii] *= R;
            cimOffs[im2].wx[ii] *= R;
            cimOffs[im2].wy[ii] *= R;
         }

         cimSearchRange = HDFsearchRange;                                  //  align search range
         cimSearchStep = HDFsearchStep;                                    //  align search step size
         cimWarpRange = HDFwarpRange;                                      //  align corner warp range
         cimWarpStep = HDFwarpStep;                                        //  align corner warp step size
      }
      
      offsets[im2].xf = cimOffs[im2].xf - cimOffs[im1].xf;                 //  save im2 offsets from im1
      offsets[im2].yf = cimOffs[im2].yf - cimOffs[im1].yf;
      offsets[im2].tf = cimOffs[im2].tf - cimOffs[im1].tf;

      for (ii = 0; ii < 4; ii++) {
         offsets[im2].wx[ii] = cimOffs[im2].wx[ii] - cimOffs[im1].wx[ii];
         offsets[im2].wy[ii] = cimOffs[im2].wy[ii] - cimOffs[im1].wy[ii];
      }
   }

   for (imx = 0; imx < cimNF; imx++)                                       //  offsets[*] >> cimOffs[*]
      cimOffs[imx] = offsets[imx];
   
   cimOffs[0].xf = cimOffs[0].yf = cimOffs[0].tf = 0;                      //  image 0 at (0,0,0)

   for (im1 = 0; im1 < cimNF-1; im1++)                                     //  absolute offsets for image 1 to last
   {
      im2 = im1 + 1;
      cimOffs[im2].xf += cimOffs[im1].xf;                                  //  x/y/t offsets are additive
      cimOffs[im2].yf += cimOffs[im1].yf;
      cimOffs[im2].tf += cimOffs[im1].tf;

      for (ii = 0; ii < 4; ii++) {                                         //  corner warps are additive
         cimOffs[im2].wx[ii] += cimOffs[im1].wx[ii];
         cimOffs[im2].wy[ii] += cimOffs[im1].wy[ii];
      }
   }

   for (imx = 1; imx < cimNF; imx++)                                       //  re-warp to absolute       v.10.8
      cim_warp_image(imx);

   toff = cimOffs[0].tf;                                                   //  balance +/- thetas
   maxtf = mintf = toff;
   for (imx = 1; imx < cimNF; imx++) {
      toff = cimOffs[imx].tf;
      if (toff > maxtf) maxtf = toff;
      if (toff < mintf) mintf = toff;
   }
   midtf = 0.5 * (maxtf + mintf);
   
   for (imx = 0; imx < cimNF; imx++)
      cimOffs[imx].tf -= midtf;

   for (im1 = 0; im1 < cimNF-1; im1++)                                     //  adjust x/y offsets for images after im1
   for (im2 = im1+1; im2 < cimNF; im2++)                                   //    due to im1 theta offset
   {
      toff = cimOffs[im1].tf;
      xoff = cimOffs[im2].xf - cimOffs[im1].xf;
      yoff = cimOffs[im2].yf - cimOffs[im1].yf;
      dxoff = yoff * sin(toff);
      dyoff = xoff * sin(toff);
      cimOffs[im2].xf -= dxoff;
      cimOffs[im2].yf += dyoff;
   }
   
   for (imx = 0; imx < cimNF; imx++)                                       //  use final warped images as basis
   {                                                                       //    for manual align adjustments
      PXM_free(cimPXMs[imx]);                                              //  bugfix     v.11.04
      cimPXMs[imx] = PXM_copy(cimPXMw[imx]);
   }

   Fzoom = Fblowup = 0;
   Ffuncbusy--;
   HDFstat = 1;
   thread_exit();
   return 0;                                                               //  not executed
}


//  paint and warp output image

zdialog  *HDFzd = 0;                                                       //  paint dialog
int      HDFmode;                                                          //  mode: paint or warp
int      HDFimage;                                                         //  current image (0 based)
int      HDFradius;                                                        //  paint mode radius
char     *HDFpixmap = 0;                                                   //  map input image per output pixel
float    *HDFwarpx[10], *HDFwarpy[10];                                     //  warp memory, pixel displacements


void HDF_tweak()                                                           //  v.10.7
{
   char     imageN[8] = "imageN", labN[4] = "0";
   int      cc, imx, ww, hh;

   int HDF_tweak_dialog_event(zdialog *zd, cchar *event);

   //    image   (o) 1  (o) 2  (o) 3  ...
   //    (o) paint  radius [___]
   //    (o) warp [__]

   HDFzd = zdialog_new(ZTX("Paint and Warp Image"),mWin,Bdone,Bcancel,null);

   zdialog_add_widget(HDFzd,"hbox","hbim","dialog",0,"space=3");
   zdialog_add_widget(HDFzd,"label","labim","hbim",ZTX("image"),"space=5");
   zdialog_add_widget(HDFzd,"hbox","hbpw","dialog",0,"space=3");
   zdialog_add_widget(HDFzd,"vbox","vbpw1","hbpw",0,"homog|space=5");
   zdialog_add_widget(HDFzd,"vbox","vbpw2","hbpw",0,"homog|space=5");
   zdialog_add_widget(HDFzd,"radio","paint","vbpw1",ZTX("paint"));
   zdialog_add_widget(HDFzd,"radio","warp","vbpw1",ZTX("warp"));
   zdialog_add_widget(HDFzd,"hbox","hbp","vbpw2");
   zdialog_add_widget(HDFzd,"label","labpr","hbp",Bradius,"space=5");
   zdialog_add_widget(HDFzd,"spin","radius","hbp","1|400|1|100");
   zdialog_add_widget(HDFzd,"label","space","vbpw2");

   for (imx = 0; imx < cimNF; imx++) {                                     //  add radio button for each image
      imageN[5] = '1' + imx;
      labN[0] = '1' + imx;
      zdialog_add_widget(HDFzd,"radio",imageN,"hbim",labN);
   }
   
   zdialog_stuff(HDFzd,"paint",1);                                         //  paint button on
   zdialog_stuff(HDFzd,"warp",0);                                          //  warp button off
   zdialog_stuff(HDFzd,"image1",1);                                        //  initial image = 1st

   HDFmode = 0;                                                            //  start in paint mode
   HDFimage = 0;                                                           //  initial image
   HDFradius = 100;                                                        //  paint radius

   takeMouse(HDFzd,HDF_mousefunc,0);                                       //  connect mouse function          v.10.12
   
   cc = E3ww * E3hh;                                                       //  allocate pixel map
   HDFpixmap = zmalloc(cc,"HDF");
   memset(HDFpixmap,cimNF,cc);                                             //  initial state, blend all images

   for (imx = 0; imx < cimNF; imx++) {                                     //  allocate warp memory
      ww = cimPXMw[imx]->ww;
      hh = cimPXMw[imx]->hh;
      HDFwarpx[imx] = (float *) zmalloc(ww * hh * sizeof(float),"HDF");
      HDFwarpy[imx] = (float *) zmalloc(ww * hh * sizeof(float),"HDF");
   }
   
   start_thread(HDF_combine_thread,0);                                     //  start working thread
   signal_thread();

   zdialog_resize(HDFzd,250,0);                                            //  stretch a bit                   v.11.07
   zdialog_run(HDFzd,HDF_tweak_dialog_event,"-10/20");                     //  run dialog, parallel            v.11.07
   zdialog_wait(HDFzd);                                                    //  wait for completion

   return;
}


//  dialog event and completion callback function

int HDF_tweak_dialog_event(zdialog *zd, cchar *event)                      //  v.10.7
{
   int      imx, nn;
   
   if (zd->zstat)                                                          //  dialog finish
   {
      freeMouse();                                                         //  disconnect mouse function    v.10.12
      signal_thread();
      wrapup_thread(8);
      if (zd->zstat == 1) HDFstat = 1;
      else HDFstat = 0;
      if (HDFstat == 1) cim_trim();                                        //  cut-off edges       v.10.9
      zdialog_free(HDFzd);
      HDFmode = 0;
      zfree(HDFpixmap);                                                    //  free pixel map
      for (imx = 0; imx < cimNF; imx++) {
         zfree(HDFwarpx[imx]);                                             //  free warp memory
         zfree(HDFwarpy[imx]);
      }
   }
   
   if (strEqu(event,"paint")) {                                            //  set paint mode
      zdialog_fetch(zd,"paint",nn);
      if (! nn) return 1;
      HDFmode = 0;
      gdk_window_set_cursor(drWin->window,0);                              //  no drag cursor            v.11.03
   }
   
   if (strEqu(event,"warp")) {                                             //  set warp mode
      zdialog_fetch(zd,"warp",nn);
      if (! nn) return 1;
      HDFmode = 1;
      paint_toparc(2);                                                     //  stop brush outline
      gdk_window_set_cursor(drWin->window,dragcursor);                     //  set drag cursor           v.11.03
   }
   
   if (strnEqu(event,"image",5)) {                                         //  image radio button
      nn = event[5] - '0';                                                 //  1 to cimNF
      if (nn > 0 && nn <= cimNF) 
         HDFimage = nn - 1;                                                //  0 to cimNF-1
      signal_thread();
   }

   if (strEqu(event,"radius"))                                             //  change paint radius
      zdialog_fetch(zd,"radius",HDFradius);

   if (strEqu(event,"focus")) {                                            //  toggle mouse capture      v.10.12
      takeMouse(zd,HDF_mousefunc,0);                                       //  connect mouse function
      if (HDFmode == 1) 
         gdk_window_set_cursor(drWin->window,dragcursor);                  //  warp mode, drag cursor    v.11.03
      signal_thread();
   }

   return 1;
}


//  HDF dialog mouse function
//  paint:  during drag, selected image >> HDFpixmap (within paint radius) >> E3
//  warp:   for selected image, cimPXMs >> warp >> cimPXMw >> E3

void HDF_mousefunc()                                                       //  v.10.7
{
   uint16      vpix1[3], *pix2, *pix3;
   int         imx, radius, radius2, vstat1;
   int         mx, my, dx, dy, px3, py3;
   char        imageN[8] = "imageN";
   double      px1, py1;
   double      xoff, yoff, sintf[10], costf[10];
   int         ii, px, py, ww, hh;
   double      mag, dispx, dispy, d1, d2;
   PXM         *pxm1, *pxm2;
   
   if (HDFmode == 0) goto paint;
   if (HDFmode == 1) goto warp;
   return;

paint:

   radius = HDFradius;                                                     //  paintbrush radius
   radius2 = radius * radius;

   toparcx = Mxposn - radius;                                              //  paintbrush outline circle
   toparcy = Myposn - radius;
   toparcw = toparch = 2 * radius;
   Ftoparc = 1;
   paint_toparc(3);

   if (LMclick || RMclick) {                                               //  mouse click
      LMclick = RMclick = 0;
      return;                                                              //  ignore     v.10.8
   }

   else if (Mxdrag || Mydrag) {                                            //  drag in progress
      mx = Mxdrag;
      my = Mydrag;
   }
   
   else return;

   if (mx < 0 || mx > E3ww-1 || my < 0 || my > E3hh-1)                     //  mouse outside image area
      return;

   for (imx = 0; imx < cimNF; imx++)                                       //  pre-calculate trig funcs
   {
      sintf[imx] = sin(cimOffs[imx].tf);
      costf[imx] = cos(cimOffs[imx].tf);
   }

   for (dy = -radius; dy <= radius; dy++)                                  //  loop pixels around mouse
   for (dx = -radius; dx <= radius; dx++)
   {
      if (dx*dx + dy*dy > radius2) continue;                               //  outside radius

      px3 = mx + dx;                                                       //  output pixel
      py3 = my + dy;
      if (px3 < 0 || px3 > E3ww-1) continue;                               //  outside image
      if (py3 < 0 || py3 > E3hh-1) continue;

      pix3 = PXMpix(E3pxm16,px3,py3);                                      //  output pixel

      imx = py3 * E3ww + px3;                                              //  update pixmap to selected image
      HDFpixmap[imx] = HDFimage;
      
      imx = HDFimage;
      xoff = cimOffs[imx].xf;
      yoff = cimOffs[imx].yf;
      px1 = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);         //  input virtual pixel
      py1 = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);
      vstat1 = vpixel(cimPXMw[imx],px1,py1,vpix1);
      if (vstat1) {
         pix3[0] = vpix1[0];
         pix3[1] = vpix1[1];
         pix3[2] = vpix1[2];
      }
      else pix3[0] = pix3[1] = pix3[2] = 0;
   }

   mx = mx - radius - 1;                                                   //  update window    v.10.12
   my = my - radius - 1;
   ww = 2 * radius + 3;
   paint_toparc(2);
   mwpaint3(mx,my,ww,ww);
   Ftoparc = 1;
   paint_toparc(3);

   return;

warp:

   if (LMclick || RMclick) {                                               //  mouse click
      LMclick = RMclick = 0;                                               //  ignore     v.10.8
      return;
   }

   else if (Mxdrag || Mydrag) {                                            //  drag in progress
      mx = Mxdrag;
      my = Mydrag;
   }
   
   else return;

   if (mx < 0 || mx > E3ww-1 || my < 0 || my > E3hh-1)                     //  mouse outside image area
      return;

   imx = my * E3ww + mx;                                                   //  if pixel has been painted, 
   imx = HDFpixmap[imx];                                                   //    select corresp. image to warp
   if (imx == cimNF) return;                                               //  else no action      v.10.8
   
   if (imx != HDFimage) {
      HDFimage = imx;                                                      //  update selected image and
      imageN[5] = '1' + imx;                                               //    dialog radio button
      zdialog_stuff(HDFzd,imageN,1);
   }

   pxm1 = cimPXMs[imx];                                                    //  input image
   pxm2 = cimPXMw[imx];                                                    //  output image
   ww = pxm2->ww;
   hh = pxm2->hh;

   mx = Mxdown;                                                            //  drag origin, image coordinates
   my = Mydown;
   dx = Mxdrag - Mxdown;                                                   //  drag increment
   dy = Mydrag - Mydown;
   Mxdown = Mxdrag;                                                        //  next drag origin
   Mydown = Mydrag;

   d1 = ww * ww + hh * hh;
   
   for (py = 0; py < hh; py++)                                             //  process all output pixels
   for (px = 0; px < ww; px++)
   {
      d2 = (px-mx)*(px-mx) + (py-my)*(py-my);
      mag = (1.0 - d2 / d1);
      mag = mag * mag * mag * mag;
      mag = mag * mag * mag * mag;
      mag = mag * mag * mag * mag;

      dispx = -dx * mag;                                                   //  displacement = drag * mag
      dispy = -dy * mag;
      
      ii = py * ww + px;
      HDFwarpx[imx][ii] += dispx;                                          //  add this drag to prior sum
      HDFwarpy[imx][ii] += dispy;

      dispx = HDFwarpx[imx][ii];
      dispy = HDFwarpy[imx][ii];

      vstat1 = vpixel(pxm1,px+dispx,py+dispy,vpix1);                       //  input virtual pixel
      pix2 = PXMpix(pxm2,px,py);                                           //  output pixel
      if (vstat1) {
         pix2[0] = vpix1[0];
         pix2[1] = vpix1[1];
         pix2[2] = vpix1[2];
      }
      else pix2[0] = pix2[1] = pix2[2] = 0;
   }

   signal_thread();                                                        //  combine images >> E3 >> main window
   return;
}


//  Combine images in E3pxm16 (not reallocated). Update main window.

void * HDF_combine_thread(void *)                                          //  v.10.7
{
   void * HDF_combine_wthread(void *);
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request

      mutex_lock(&Fpixmap_lock);                                           //  stop window updates

      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads   v.10.7
         start_wthread(HDF_combine_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      mutex_unlock(&Fpixmap_lock);                                         //  update window
      mwpaint2();
   }

   return 0;                                                               //  not executed
}


void * HDF_combine_wthread(void *arg)                                      //  worker thread
{
   int         index = *((int *) (arg));                                   //  no more paint and warp modes  v.10.8
   int         px3, py3, vstat1;
   int         imx, red, green, blue;
   double      px, py;
   double      xoff, yoff, sintf[10], costf[10];
   uint16      vpix1[3], *pix3;

   for (imx = 0; imx < cimNF; imx++)                                       //  pre-calculate trig funcs
   {
      sintf[imx] = sin(cimOffs[imx].tf);
      costf[imx] = cos(cimOffs[imx].tf);
   }

   for (py3 = index+1; py3 < E3hh-1; py3 += Nwt)                           //  step through output pixels
   for (px3 = 1; px3 < E3ww-1; px3++)
   {
      pix3 = PXMpix(E3pxm16,px3,py3);
      
      imx = py3 * E3ww + px3;
      imx = HDFpixmap[imx];

      if (imx < cimNF)                                                     //  specific image maps to pixel
      {
         xoff = cimOffs[imx].xf;
         yoff = cimOffs[imx].yf;
         px = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);
         py = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);
         vstat1 = vpixel(cimPXMw[imx],px,py,vpix1);                        //  corresp. input vpixel
         if (vstat1) {
            pix3[0] = vpix1[0];
            pix3[1] = vpix1[1];
            pix3[2] = vpix1[2];
         }
         else pix3[0] = pix3[1] = pix3[2] = 0;
      }
      
      else                                                                 //  use blend of all images
      {
         red = green = blue = 0;

         for (imx = 0; imx < cimNF; imx++)
         {
            xoff = cimOffs[imx].xf;
            yoff = cimOffs[imx].yf;
            px = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);
            py = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);
            vstat1 = vpixel(cimPXMw[imx],px,py,vpix1);
            if (vstat1) {
               red += vpix1[0];
               green += vpix1[1];
               blue += vpix1[2];
            }
         }
         
         pix3[0] = red / cimNF;
         pix3[1] = green / cimNF;
         pix3[2] = blue / cimNF;
      }
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


/**************************************************************************

   Stack/Paint function
   Combine multiple images of one subject taken at different times from
   (almost) the same camera position. Align the images and allow the user 
   to choose which input image to use for each area of the output image, 
   by "painting" with the mouse. Use this to remove tourists and cars that
   move in and out of a scene being photographed.
   
**************************************************************************/

int      STPstat;                                                          //  1 = OK, 0 = failed or canceled
double   STPinitAlignSize = 160;                                           //  initial align image size
double   STPimageIncrease = 1.6;                                           //  image size increase per align cycle
double   STPsampSize = 10000;                                              //  pixel sample size    v.11.03

double   STPinitSearchRange = 5.0;                                         //  initial search range, +/- pixels
double   STPinitSearchStep = 1.0;                                          //  initial search step, pixels
double   STPinitWarpRange = 2.0;                                           //  initial corner warp range
double   STPinitWarpStep = 1.0;                                            //  initial corner warp step
double   STPsearchRange = 2.0;                                             //  normal search range
double   STPsearchStep = 1.0;                                              //  normal search step
double   STPwarpRange = 1.0;                                               //  normal corner warp range
double   STPwarpStep = 0.67;                                               //  normal corner warp step

void * STP_align_thread(void *);
void   STP_tweak();
void   STP_mousefunc();
void * STP_combine_thread(void *);

editfunc    EFstp;                                                         //  edit function data


//  menu function

void m_STP(GtkWidget *, cchar *)                                           //  v.11.02
{
   char        **flist;
   int         imx, err, ww, hh;
   double      diffw, diffh;
   
   zfuncs::F1_help_topic = "stack_paint";                                  //  help topic

   if (mod_keep()) return;                                                 //  warn unsaved changes
   if (! menulock(1)) return;                                              //  test menu lock            v.11.07
   menulock(0);

   for (imx = 0; imx < 10; imx++)
   {                                                                       //  clear all file and PXM data
      cimFile[imx] = 0;
      cimPXMf[imx] = cimPXMs[imx] = cimPXMw[imx] = 0;
   }

   cimNF = 0;   

   flist = zgetfileN(ZTX("Select 2 to 9 files"),"openN",curr_file);        //  select images to combine
   if (! flist) return;

   for (imx = 0; flist[imx]; imx++);                                       //  count selected files
   if (imx < 2 || imx > 9) {
      zmessageACK(mWin,ZTX("Select 2 to 9 files"));
      goto cleanup;
   }

   cimNF = imx;                                                            //  file count
   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = strdupz(flist[imx],0,"STP");                          //  set up file list
   
   if (! cim_load_files()) goto cleanup;                                   //  load and check all files

   ww = cimPXMf[0]->ww;
   hh = cimPXMf[0]->hh;
   
   for (imx = 1; imx < cimNF; imx++)                                       //  check image compatibility
   {
      diffw = abs(ww - cimPXMf[imx]->ww);
      diffw = diffw / ww;
      diffh = abs(hh - cimPXMf[imx]->hh);
      diffh = diffh / hh;

      if (diffw > 0.02 || diffh > 0.02) {
         zmessageACK(mWin,ZTX("Images are not all the same size"));
         goto cleanup;
      }
   }

   free_resources();                                                       //  ready to commit

   err = f_open(cimFile[0],0);                                             //  curr_file = 1st file in list
   if (err) goto cleanup;

   EFstp.funcname = "stack-paint";
   if (! edit_setup(EFstp)) goto cleanup;                                  //  setup edit (will lock)

   start_thread(STP_align_thread,0);                                       //  align each pair of images
   wrapup_thread(0);                                                       //  wait for completion
   if (STPstat != 1) goto cancel;

   STP_tweak();                                                            //  combine images based on user inputs
   if (STPstat != 1) goto cancel;

   CEF->Fmod = 1;                                                          //  done
   edit_done(EFstp);
   goto cleanup;

cancel:
   edit_cancel(EFstp);

cleanup:

   if (flist) {
      for (imx = 0; flist[imx]; imx++)                                     //  free file list
         zfree(flist[imx]);
      zfree(flist);
   }

   for (imx = 0; imx < cimNF; imx++) {                                     //  free cim file and PXM data
      if (cimFile[imx]) zfree(cimFile[imx]);
      if (cimPXMf[imx]) PXM_free(cimPXMf[imx]);
      if (cimPXMs[imx]) PXM_free(cimPXMs[imx]);
      if (cimPXMw[imx]) PXM_free(cimPXMw[imx]);
   }
   
   *SB_text = 0;
   return;
}


//  align each pair of input images, output combined image to E3pxm16
//  cimPXMf[*]  original image
//  cimPXMs[*]  scaled and color adjusted for pixel comparisons
//  cimPXMw[*]  warped for display

void * STP_align_thread(void *)                                            //  v.11.02
{
   int         imx, im1, im2, ww, hh, ii, nn;
   double      R, maxtf, mintf, midtf;
   double      xoff, yoff, toff, dxoff, dyoff;
   cimoffs     offsets[10];                                                //  x/y/t offsets after alignment
   
   Fzoom = 0;                                                              //  fit to window if big
   Fblowup = 1;                                                            //  scale up to window if small
   Ffuncbusy++;
   cimShrink = 0;                                                          //  no warp shrinkage (pano)
   cimPano = cimPanoV = 0;                                                 //  no pano mode

   for (imx = 0; imx < cimNF; imx++)
      memset(&offsets[imx],0,sizeof(cimoffs));
   
   for (im1 = 0; im1 < cimNF-1; im1++)                                     //  loop each pair of images
   {
      im2 = im1 + 1;

      memset(&cimOffs[im1],0,sizeof(cimoffs));                             //  initial image offsets = 0
      memset(&cimOffs[im2],0,sizeof(cimoffs));
      
      ww = cimPXMf[im1]->ww;                                               //  image dimensions
      hh = cimPXMf[im1]->hh;

      nn = ww;                                                             //  use larger of ww, hh
      if (hh > ww) nn = hh;
      cimScale = STPinitAlignSize / nn;                                    //  initial align image size
      if (cimScale > 1.0) cimScale = 1.0;

      cimBlend = 0;                                                        //  no blend width (use all)
      cim_get_overlap(im1,im2,cimPXMf);                                    //  get overlap area
      cim_match_colors(im1,im2,cimPXMf);                                   //  get color matching factors

      cimSearchRange = STPinitSearchRange;                                 //  initial align search range
      cimSearchStep = STPinitSearchStep;                                   //  initial align search step
      cimWarpRange = STPinitWarpRange;                                     //  initial align corner warp range
      cimWarpStep = STPinitWarpStep;                                       //  initial align corner warp step
      cimSampSize = STPsampSize;                                           //  pixel sample size for align/compare
      cimNsearch = 0;                                                      //  reset align search counter

      while (true)                                                         //  loop, increasing image size
      {
         cim_scale_image(im1,cimPXMs);                                     //  scale images to cimScale
         cim_scale_image(im2,cimPXMs);

         cim_adjust_colors(cimPXMs[im1],1);                                //  apply color adjustments
         cim_adjust_colors(cimPXMs[im2],2);

         cim_warp_image(im1);                                              //  warp images for show
         cim_warp_image(im2);

         cimShowIm1 = im1;                                                 //  show these two images
         cimShowIm2 = im2;                                                 //    with 50/50 blend
         cimShowAll = 0;
         cim_show_images(1,0);                                             //  (y offset can change)

         cim_get_overlap(im1,im2,cimPXMs);                                 //  get overlap area             v.11.04
         cim_get_redpix(im1);                                              //  get high-contrast pixels

         cim_align_image(im1,im2);                                         //  align im2 to im1
         
         zfree(cimRedpix);                                                 //  clear red pixels
         cimRedpix = 0;

         if (cimScale == 1.0) break;                                       //  done

         R = STPimageIncrease;                                             //  next larger image size
         cimScale = cimScale * R;
         if (cimScale > 0.85) {                                            //  if close to end, jump to end
            R = R / cimScale;
            cimScale = 1.0;
         }

         cimOffs[im1].xf *= R;                                             //  scale offsets for larger image
         cimOffs[im1].yf *= R;
         cimOffs[im2].xf *= R;
         cimOffs[im2].yf *= R;

         for (ii = 0; ii < 4; ii++) {
            cimOffs[im1].wx[ii] *= R;
            cimOffs[im1].wy[ii] *= R;
            cimOffs[im2].wx[ii] *= R;
            cimOffs[im2].wy[ii] *= R;
         }

         cimSearchRange = STPsearchRange;                                  //  align search range
         cimSearchStep = STPsearchStep;                                    //  align search step size
         cimWarpRange = STPwarpRange;                                      //  align corner warp range
         cimWarpStep = STPwarpStep;                                        //  align corner warp step size
      }

      offsets[im2].xf = cimOffs[im2].xf - cimOffs[im1].xf;                 //  save im2 offsets from im1
      offsets[im2].yf = cimOffs[im2].yf - cimOffs[im1].yf;
      offsets[im2].tf = cimOffs[im2].tf - cimOffs[im1].tf;

      for (ii = 0; ii < 4; ii++) {
         offsets[im2].wx[ii] = cimOffs[im2].wx[ii] - cimOffs[im1].wx[ii];
         offsets[im2].wy[ii] = cimOffs[im2].wy[ii] - cimOffs[im1].wy[ii];
      }
   }

   for (imx = 0; imx < cimNF; imx++)                                       //  offsets[*] >> cimOffs[*]
      cimOffs[imx] = offsets[imx];
   
   cimOffs[0].xf = cimOffs[0].yf = cimOffs[0].tf = 0;                      //  image 0 at (0,0,0)

   for (im1 = 0; im1 < cimNF-1; im1++)                                     //  absolute offsets for image 1 to last
   {
      im2 = im1 + 1;
      cimOffs[im2].xf += cimOffs[im1].xf;                                  //  x/y/t offsets are additive
      cimOffs[im2].yf += cimOffs[im1].yf;
      cimOffs[im2].tf += cimOffs[im1].tf;

      for (ii = 0; ii < 4; ii++) {                                         //  corner warps are additive
         cimOffs[im2].wx[ii] += cimOffs[im1].wx[ii];
         cimOffs[im2].wy[ii] += cimOffs[im1].wy[ii];
      }
   }

   for (imx = 1; imx < cimNF; imx++)                                       //  re-warp to absolute
      cim_warp_image(imx);

   toff = cimOffs[0].tf;                                                   //  balance +/- thetas
   maxtf = mintf = toff;
   for (imx = 1; imx < cimNF; imx++) {
      toff = cimOffs[imx].tf;
      if (toff > maxtf) maxtf = toff;
      if (toff < mintf) mintf = toff;
   }
   midtf = 0.5 * (maxtf + mintf);
   
   for (imx = 0; imx < cimNF; imx++)
      cimOffs[imx].tf -= midtf;

   for (im1 = 0; im1 < cimNF-1; im1++)                                     //  adjust x/y offsets for images after im1
   for (im2 = im1+1; im2 < cimNF; im2++)                                   //    due to im1 theta offset
   {
      toff = cimOffs[im1].tf;
      xoff = cimOffs[im2].xf - cimOffs[im1].xf;
      yoff = cimOffs[im2].yf - cimOffs[im1].yf;
      dxoff = yoff * sin(toff);
      dyoff = xoff * sin(toff);
      cimOffs[im2].xf -= dxoff;
      cimOffs[im2].yf += dyoff;
   }

   Fzoom = Fblowup = 0;
   Ffuncbusy--;
   STPstat = 1;
   thread_exit();
   return 0;                                                               //  not executed
}


//  paint output image

zdialog  *STPzd = 0;                                                       //  paint dialog
int      STPimage;                                                         //  current image (0 based)
int      STPradius;                                                        //  paint mode radius
char     *STPpixmap = 0;                                                   //  map input image per output pixel


void STP_tweak()                                                           //  v.11.02
{
   char     imageN[8] = "imageN", labN[4] = "0";
   int      cc, imx;

   int STP_tweak_dialog_event(zdialog *zd, cchar *event);

   //    image   (o) 1  (o) 2  (o) 3  ...
   //    radius [___]

   STPzd = zdialog_new(ZTX("Select and Paint Image"),mWin,Bdone,Bcancel,null);
   zdialog_add_widget(STPzd,"hbox","hbim","dialog",0,"space=3");
   zdialog_add_widget(STPzd,"label","labim","hbim",ZTX("image"),"space=5");
   zdialog_add_widget(STPzd,"hbox","hbmr","dialog",0,"space=3");
   zdialog_add_widget(STPzd,"label","labr","hbmr",Bradius,"space=5");
   zdialog_add_widget(STPzd,"spin","radius","hbmr","1|400|1|100");

   for (imx = 0; imx < cimNF; imx++) {                                     //  add radio button for each image
      imageN[5] = '1' + imx;
      labN[0] = '1' + imx;
      zdialog_add_widget(STPzd,"radio",imageN,"hbim",labN);
   }
   
   zdialog_stuff(STPzd,"image1",1);                                        //  initial image = 1st

   STPimage = 0;                                                           //  initial image
   STPradius = 100;                                                        //  paint radius

   takeMouse(STPzd,STP_mousefunc,0);                                       //  connect mouse function
   
   cc = E3ww * E3hh;                                                       //  allocate pixel map
   STPpixmap = zmalloc(cc,"STP");
   memset(STPpixmap,cimNF,cc);                                             //  initial state, blend all images

   start_thread(STP_combine_thread,0);                                     //  start working thread
   signal_thread();

   zdialog_run(STPzd,STP_tweak_dialog_event,"-10/20");                     //  run dialog, parallel            v.11.07
   zdialog_wait(STPzd);                                                    //  wait for completion

   return;
}


//  dialog event and completion callback function

int STP_tweak_dialog_event(zdialog *zd, cchar *event)                      //  v.11.02
{
   int      nn;
   
   if (zd->zstat)                                                          //  dialog finish
   {
      freeMouse();                                                         //  disconnect mouse function
      signal_thread();
      wrapup_thread(8);
      if (zd->zstat == 1) STPstat = 1;
      else STPstat = 0;
      if (STPstat == 1) cim_trim();                                        //  cut-off edges
      zdialog_free(STPzd);
      zfree(STPpixmap);                                                    //  free pixel map
   }
   
   if (strnEqu(event,"image",5)) {                                         //  image radio button
      nn = event[5] - '0';                                                 //  1 to cimNF
      if (nn > 0 && nn <= cimNF) 
         STPimage = nn - 1;                                                //  0 to cimNF-1
      signal_thread();
   }

   if (strEqu(event,"radius"))                                             //  change paint radius
      zdialog_fetch(zd,"radius",STPradius);

   if (strEqu(event,"focus")) {                                            //  toggle mouse capture      v.12.01
      takeMouse(zd,STP_mousefunc,0);                                       //  connect mouse function
      signal_thread();
   }

   return 1;
}


//  STP dialog mouse function
//  paint:  during drag, selected image >> STPpixmap (within paint radius) >> E3
//  warp:   for selected image, cimPXMs >> warp >> cimPXMw >> E3

void STP_mousefunc()                                                       //  v.11.02
{
   uint16      vpix1[3], *pix3;
   int         imx, radius, radius2, vstat1;
   int         mx, my, dx, dy, px3, py3, ww;
   double      px1, py1;
   double      xoff, yoff, sintf[10], costf[10];
   
   radius = STPradius;                                                     //  paintbrush radius
   radius2 = radius * radius;

   toparcx = Mxposn - radius;                                              //  paintbrush outline circle
   toparcy = Myposn - radius;
   toparcw = toparch = 2 * radius;
   Ftoparc = 1;
   paint_toparc(3);

   if (LMclick || RMclick) {                                               //  mouse click
      LMclick = RMclick = 0;
      return;                                                              //  ignore
   }

   else if (Mxdrag || Mydrag) {                                            //  drag in progress
      mx = Mxdrag;
      my = Mydrag;
   }
   
   else return;

   if (mx < 0 || mx > E3ww-1 || my < 0 || my > E3hh-1)                     //  mouse outside image area
      return;

   for (imx = 0; imx < cimNF; imx++)                                       //  pre-calculate trig funcs
   {
      sintf[imx] = sin(cimOffs[imx].tf);
      costf[imx] = cos(cimOffs[imx].tf);
   }

   for (dy = -radius; dy <= radius; dy++)                                  //  loop pixels around mouse
   for (dx = -radius; dx <= radius; dx++)
   {
      if (dx*dx + dy*dy > radius2) continue;                               //  outside radius

      px3 = mx + dx;                                                       //  output pixel
      py3 = my + dy;
      if (px3 < 0 || px3 > E3ww-1) continue;                               //  outside image
      if (py3 < 0 || py3 > E3hh-1) continue;

      pix3 = PXMpix(E3pxm16,px3,py3);                                      //  output pixel

      imx = py3 * E3ww + px3;                                              //  update pixmap to selected image
      STPpixmap[imx] = STPimage;
      
      imx = STPimage;
      xoff = cimOffs[imx].xf;
      yoff = cimOffs[imx].yf;
      px1 = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);         //  input virtual pixel
      py1 = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);
      vstat1 = vpixel(cimPXMw[imx],px1,py1,vpix1);
      if (vstat1) {
         pix3[0] = vpix1[0];
         pix3[1] = vpix1[1];
         pix3[2] = vpix1[2];
      }
      else pix3[0] = pix3[1] = pix3[2] = 0;
   }

   mx = mx - radius - 1;                                                   //  update window
   my = my - radius - 1;
   ww = 2 * radius + 3;
   paint_toparc(2);
   mwpaint3(mx,my,ww,ww);
   Ftoparc = 1;
   paint_toparc(3);
   return;
}


//  Combine images in E3pxm16 (not reallocated). Update main window.

void * STP_combine_thread(void *)                                          //  v.11.02
{
   void * STP_combine_wthread(void *);
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request

      mutex_lock(&Fpixmap_lock);                                           //  stop window updates

      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(STP_combine_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      mutex_unlock(&Fpixmap_lock);                                         //  update window
      mwpaint2();
   }

   return 0;                                                               //  not executed
}


void * STP_combine_wthread(void *arg)                                      //  worker thread
{
   int         index = *((int *) (arg));
   int         px3, py3, vstat1;
   int         imx, red, green, blue;
   double      px, py;
   double      xoff, yoff, sintf[10], costf[10];
   uint16      vpix1[3], *pix3;

   for (imx = 0; imx < cimNF; imx++)                                       //  pre-calculate trig funcs
   {
      sintf[imx] = sin(cimOffs[imx].tf);
      costf[imx] = cos(cimOffs[imx].tf);
   }

   for (py3 = index+1; py3 < E3hh-1; py3 += Nwt)                           //  step through output pixels
   for (px3 = 1; px3 < E3ww-1; px3++)
   {
      pix3 = PXMpix(E3pxm16,px3,py3);
      
      imx = py3 * E3ww + px3;
      imx = STPpixmap[imx];

      if (imx < cimNF)                                                     //  specific image maps to pixel
      {
         xoff = cimOffs[imx].xf;
         yoff = cimOffs[imx].yf;
         px = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);
         py = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);
         vstat1 = vpixel(cimPXMw[imx],px,py,vpix1);                        //  corresp. input vpixel
         if (vstat1) {
            pix3[0] = vpix1[0];
            pix3[1] = vpix1[1];
            pix3[2] = vpix1[2];
         }
         else pix3[0] = pix3[1] = pix3[2] = 0;
      }
      
      else                                                                 //  use blend of all images
      {
         red = green = blue = 0;

         for (imx = 0; imx < cimNF; imx++)
         {
            xoff = cimOffs[imx].xf;
            yoff = cimOffs[imx].yf;
            px = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);
            py = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);
            vstat1 = vpixel(cimPXMw[imx],px,py,vpix1);
            if (vstat1) {
               red += vpix1[0];
               green += vpix1[1];
               blue += vpix1[2];
            }
         }
         
         pix3[0] = red / cimNF;
         pix3[1] = green / cimNF;
         pix3[2] = blue / cimNF;
      }
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


/**************************************************************************

   Stack/Noise function
   Combine multiple photos of the same subject and average the 
   pixels for noise reduction.

**************************************************************************/

double   STN_initAlignSize = 160;                                          //  initial align image size
double   STN_imageIncrease = 1.6;                                          //  image size increase per align cycle
double   STN_sampSize = 6000;                                              //  pixel sample size

double   STN_initSearchRange = 5.0;                                        //  initial search range, +/- pixels
double   STN_initSearchStep = 1.0;                                         //  initial search step, pixels
double   STN_initWarpRange = 2.0;                                          //  initial corner warp range
double   STN_initWarpStep = 1.0;                                           //  initial corner warp step
double   STN_searchRange = 2.0;                                            //  normal search range
double   STN_searchStep = 1.0;                                             //  normal search step
double   STN_warpRange = 1.0;                                              //  normal corner warp range
double   STN_warpStep = 0.67;                                              //  normal corner warp step

int      STN_stat;                                                         //  1 = OK, 0 = failed or canceled
int      STN_average = 1, STN_median = 0;                                  //  use average/median of input pixels
int      STN_exlow = 0, STN_exhigh = 0;                                    //  exclude low/high pixel

void * STN_align_thread(void *);
void   STN_tweak();
void * STN_combine_thread(void *);

editfunc    EFstn;                                                         //  edit function data


//  menu function

void m_STN(GtkWidget *, cchar *)                                           //  new v.10.9
{
   char        **flist;
   int         imx, err, ww, hh;
   double      diffw, diffh;
   
   zfuncs::F1_help_topic = "stack_noise";                                  //  help topic

   if (mod_keep()) return;                                                 //  warn unsaved changes
   if (! menulock(1)) return;                                              //  test menu lock            v.11.07
   menulock(0);

   for (imx = 0; imx < 10; imx++)
   {                                                                       //  clear all file and PXM data
      cimFile[imx] = 0;
      cimPXMf[imx] = cimPXMs[imx] = cimPXMw[imx] = 0;
   }

   cimNF = 0;   

   flist = zgetfileN(ZTX("Select 2 to 9 files"),"openN",curr_file);        //  select images to combine
   if (! flist) return;

   for (imx = 0; flist[imx]; imx++);                                       //  count selected files
   if (imx < 2 || imx > 9) {
      zmessageACK(mWin,ZTX("Select 2 to 9 files"));
      goto cleanup;
   }

   cimNF = imx;                                                            //  file count
   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = strdupz(flist[imx],0,"STN");                          //  set up file list
   
   if (! cim_load_files()) goto cleanup;                                   //  load and check all files

   ww = cimPXMf[0]->ww;
   hh = cimPXMf[0]->hh;
   
   for (imx = 1; imx < cimNF; imx++)                                       //  check image compatibility
   {
      diffw = abs(ww - cimPXMf[imx]->ww);
      diffw = diffw / ww;
      diffh = abs(hh - cimPXMf[imx]->hh);
      diffh = diffh / hh;

      if (diffw > 0.02 || diffh > 0.02) {
         zmessageACK(mWin,ZTX("Images are not all the same size"));
         goto cleanup;
      }
   }

   free_resources();                                                       //  ready to commit

   err = f_open(cimFile[0],0);                                             //  curr_file = 1st file in list
   if (err) goto cleanup;

   EFstn.funcname = "stack-noise";
   if (! edit_setup(EFstn)) goto cleanup;                                  //  setup edit (will lock)

   start_thread(STN_align_thread,0);                                       //  align each pair of images
   wrapup_thread(0);                                                       //  wait for completion
   if (STN_stat != 1) goto cancel;

   STN_tweak();                                                            //  combine images based on user inputs
   if (STN_stat != 1) goto cancel;

   CEF->Fmod = 1;                                                          //  done
   edit_done(EFstn);
   goto cleanup;

cancel:
   edit_cancel(EFstn);

cleanup:

   if (flist) {
      for (imx = 0; flist[imx]; imx++)                                     //  free file list
         zfree(flist[imx]);
      zfree(flist);
   }

   for (imx = 0; imx < cimNF; imx++) {                                     //  free cim file and PXM data
      if (cimFile[imx]) zfree(cimFile[imx]);
      if (cimPXMf[imx]) PXM_free(cimPXMf[imx]);
      if (cimPXMs[imx]) PXM_free(cimPXMs[imx]);
      if (cimPXMw[imx]) PXM_free(cimPXMw[imx]);
   }
   
   *SB_text = 0;
   return;
}


//  align each image 2nd-last to 1st image
//  cimPXMf[*]  original image
//  cimPXMs[*]  scaled and color adjusted for pixel comparisons
//  cimPXMw[*]  warped for display

void * STN_align_thread(void *)                                            //  v.10.9
{
   int         imx, im1, im2, ww, hh, ii, nn;
   double      R, maxtf, mintf, midtf;
   double      xoff, yoff, toff, dxoff, dyoff;
   
   Fzoom = 0;                                                              //  fit to window if big
   Fblowup = 1;                                                            //  scale up to window if small
   Ffuncbusy++;                                                            //  v.11.01
   cimShrink = 0;                                                          //  no warp shrinkage (pano)
   cimPano = cimPanoV = 0;                                                 //  no pano mode

   for (imx = 1; imx < cimNF; imx++)                                       //  loop 2nd to last image
   {
      im1 = 0;                                                             //  images to align
      im2 = imx;

      memset(&cimOffs[im1],0,sizeof(cimoffs));                             //  initial image offsets = 0
      memset(&cimOffs[im2],0,sizeof(cimoffs));
      
      ww = cimPXMf[im1]->ww;                                               //  image dimensions
      hh = cimPXMf[im1]->hh;

      nn = ww;                                                             //  use larger of ww, hh
      if (hh > ww) nn = hh;
      cimScale = STN_initAlignSize / nn;                                   //  initial align image size
      if (cimScale > 1.0) cimScale = 1.0;

      cimBlend = 0;                                                        //  no blend width (use all)
      cim_get_overlap(im1,im2,cimPXMf);                                    //  get overlap area
      cim_match_colors(im1,im2,cimPXMf);                                   //  get color matching factors

      cimSearchRange = STN_initSearchRange;                                //  initial align search range
      cimSearchStep = STN_initSearchStep;                                  //  initial align search step
      cimWarpRange = STN_initWarpRange;                                    //  initial align corner warp range
      cimWarpStep = STN_initWarpStep;                                      //  initial align corner warp step
      cimSampSize = STN_sampSize;                                          //  pixel sample size for align/compare
      cimNsearch = 0;                                                      //  reset align search counter

      while (true)                                                         //  loop, increasing image size
      {
         cim_scale_image(im1,cimPXMs);                                     //  scale images to cimScale
         cim_scale_image(im2,cimPXMs);

         cim_adjust_colors(cimPXMs[im1],1);                                //  apply color adjustments
         cim_adjust_colors(cimPXMs[im2],2);

         cim_warp_image(im1);                                              //  warp images for show
         cim_warp_image(im2);

         cimShowIm1 = im1;                                                 //  show these two images
         cimShowIm2 = im2;                                                 //    with 50/50 blend
         cimShowAll = 0;
         cim_show_images(1,0);                                             //  (y offset can change)

         cim_get_overlap(im1,im2,cimPXMs);                                 //  get overlap area             v.11.04
         cim_get_redpix(im1);                                              //  get high-contrast pixels

         cim_align_image(im1,im2);                                         //  align im2 to im1
         
         zfree(cimRedpix);                                                 //  clear red pixels
         cimRedpix = 0;

         if (cimScale == 1.0) break;                                       //  done

         R = STN_imageIncrease;                                            //  next larger image size
         cimScale = cimScale * R;
         if (cimScale > 0.85) {                                            //  if close to end, jump to end
            R = R / cimScale;
            cimScale = 1.0;
         }

         cimOffs[im1].xf *= R;                                             //  scale offsets for larger image
         cimOffs[im1].yf *= R;
         cimOffs[im2].xf *= R;
         cimOffs[im2].yf *= R;

         for (ii = 0; ii < 4; ii++) {
            cimOffs[im1].wx[ii] *= R;
            cimOffs[im1].wy[ii] *= R;
            cimOffs[im2].wx[ii] *= R;
            cimOffs[im2].wy[ii] *= R;
         }

         cimSearchRange = STN_searchRange;                                 //  align search range
         cimSearchStep = STN_searchStep;                                   //  align search step size
         cimWarpRange = STN_warpRange;                                     //  align corner warp range
         cimWarpStep = STN_warpStep;                                       //  align corner warp step size
      }
   }

   toff = cimOffs[0].tf;                                                   //  balance +/- thetas
   maxtf = mintf = toff;
   for (imx = 1; imx < cimNF; imx++) {
      toff = cimOffs[imx].tf;
      if (toff > maxtf) maxtf = toff;
      if (toff < mintf) mintf = toff;
   }
   midtf = 0.5 * (maxtf + mintf);
   
   for (imx = 0; imx < cimNF; imx++)
      cimOffs[imx].tf -= midtf;

   for (im1 = 0; im1 < cimNF-1; im1++)                                     //  adjust x/y offsets for images after im1
   for (im2 = im1+1; im2 < cimNF; im2++)                                   //    due to im1 theta offset
   {
      toff = cimOffs[im1].tf;
      xoff = cimOffs[im2].xf - cimOffs[im1].xf;
      yoff = cimOffs[im2].yf - cimOffs[im1].yf;
      dxoff = yoff * sin(toff);
      dyoff = xoff * sin(toff);
      cimOffs[im2].xf -= dxoff;
      cimOffs[im2].yf += dyoff;
   }

   Fzoom = Fblowup = 0;
   Ffuncbusy--;
   STN_stat = 1;
   thread_exit();
   return 0;                                                               //  not executed
}


//  change pixel combination according to user input

void STN_tweak()                                                           //  v.10.9
{
   zdialog     *zd;

   int STN_tweak_dialog_event(zdialog *zd, cchar *event);

   //    Adjust Pixel Composition
   //
   //    (o) use average  (o) use median
   //    [x] omit lowest value
   //    [x] omit highest value

   zd = zdialog_new(ZTX("Adjust Pixel Composition"),mWin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","average","hb1","use average","space=3");
   zdialog_add_widget(zd,"radio","median","hb1","use median","space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","exlow","hb2","omit low pixel","space=3");
   zdialog_add_widget(zd,"check","exhigh","hb2","omit high pixel","space=3");
   
   zdialog_stuff(zd,"average",1);                                          //  default = average
   zdialog_stuff(zd,"median",0);
   zdialog_stuff(zd,"exlow",0);
   zdialog_stuff(zd,"exhigh",0);
   
   STN_average = 1;
   STN_median = 0;
   STN_exlow = 0;
   STN_exhigh = 0;
   
   start_thread(STN_combine_thread,0);                                     //  start working thread
   signal_thread();

   zdialog_resize(zd,250,0);
   zdialog_run(zd,STN_tweak_dialog_event,"-10/20");                        //  run dialog, parallel            v.11.07
   zdialog_wait(zd);                                                       //  wait for completion

   return;
}


//  dialog event and completion callback function

int STN_tweak_dialog_event(zdialog *zd, cchar *event)                      //  v.10.9
{
   if (zd->zstat) {                                                        //  dialog finish
      if (zd->zstat == 1) STN_stat = 1;
      else STN_stat = 0;
      wrapup_thread(8);
      zdialog_free(zd);
      if (STN_stat == 1) cim_trim();                                       //  trim edges    v.10.9
   }
   
   if (strEqu(event,"average")) {
      zdialog_fetch(zd,"average",STN_average);
      signal_thread();
   }
   
   if (strEqu(event,"median")) {
      zdialog_fetch(zd,"median",STN_median);
      signal_thread();
   }
   
   if (strEqu(event,"exlow")) {
      zdialog_fetch(zd,"exlow",STN_exlow);
      signal_thread();
   }
   
   if (strEqu(event,"exhigh")) {
      zdialog_fetch(zd,"exhigh",STN_exhigh);
      signal_thread();
   }
   
   return 1;
}


//  compute mean/median mix for each output pixel and update E3 image

void * STN_combine_thread(void *)                                          //  v.10.9
{
   void * STN_combine_wthread(void *arg);                                  //  worker thread

   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request

      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(STN_combine_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

   return 0;                                                               //  not executed
}


//  worker thread

void * STN_combine_wthread(void *arg)                                      //  v.10.9
{
   int         index = *((int *) arg);
   int         imx, vstat, px3, py3;
   int         red, green, blue;
   int         ii, ns, ns1, ns2;
   int         Rlist[10], Glist[10], Blist[10];
   double      px, py;
   double      xoff, yoff, sintf[10], costf[10];
   uint16      *pix3, vpix[3];

   //    input layers     0      1      2      3      4      5      6      7      8      9     10
   int   nsx[11][2] = { {0,0}, {0,0}, {0,1}, {1,1}, {1,2}, {2,2}, {2,3}, {2,4}, {2,5}, {3,5}, {3,6} };
   
   for (imx = 0; imx < cimNF; imx++)                                       //  pre-calculate trig funcs
   {
      sintf[imx] = sin(cimOffs[imx].tf);
      costf[imx] = cos(cimOffs[imx].tf);
   }

   for (py3 = index+1; py3 < E3hh-1; py3 += Nwt)                           //  step through output pixels
   for (px3 = 1; px3 < E3ww-1; px3++)
   {
      for (imx = ns = 0; imx < cimNF; imx++)                               //  get aligned input pixels
      {
         xoff = cimOffs[imx].xf;
         yoff = cimOffs[imx].yf;
         px = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);
         py = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);

         vstat = vpixel(cimPXMw[imx],px,py,vpix);
         if (vstat) {
            Rlist[ns] = vpix[0];                                           //  add pixel RGB values to list
            Glist[ns] = vpix[1];
            Blist[ns] = vpix[2];
            ns++;
         }
      }
      
      if (! ns) continue;
      
      if (STN_exlow || STN_exhigh || STN_median) {                         //  RGB values must be sorted
         HeapSort(Rlist,ns);
         HeapSort(Glist,ns);
         HeapSort(Blist,ns);
      }

      red = green = blue = 0;

      if (STN_average)                                                     //  average the input pixels
      {
         ns1 = 0;                                                          //  low and high RGB values
         ns2 = ns - 1;
         
         if (STN_exlow) {                                                  //  exclude low
            ns1++;
            if (ns1 > ns2) ns1--;
         }

         if (STN_exhigh) {                                                 //  exclude high
            ns2--;
            if (ns1 > ns2) ns2++;
         }
         
         for (ii = ns1; ii <= ns2; ii++)                                   //  sum remaining RGB levels
         {
            red += Rlist[ii];
            green += Glist[ii];
            blue += Blist[ii];
         }

         ns = ns2 - ns1 + 1;                                               //  sample count

         red = red / ns;                                                   //  output RGB = average
         green = green / ns;
         blue = blue / ns;
      }
      
      if (STN_median)                                                      //  use median input pixels
      {
         ns1 = nsx[ns][0];                                                 //  middle group of pixels
         ns2 = nsx[ns][1];
         
         for (ii = ns1; ii <= ns2; ii++)
         {
            red += Rlist[ii];
            green += Glist[ii];
            blue += Blist[ii];
         }

         ns = ns2 - ns1 + 1;                                               //  sample count

         red = red / ns;                                                   //  output RGB = average
         green = green / ns;
         blue = blue / ns;
      }

      pix3 = PXMpix(E3pxm16,px3,py3);                                      //  output pixel
      pix3[0] = red;
      pix3[1] = green;
      pix3[2] = blue;
   }

   exit_wthread();
   return 0;                                                               //  not executed
}


/**************************************************************************

    Panorama function: join 2, 3, or 4 images.

***************************************************************************/

int      panStat;                                                          //  1 = OK
zdialog  *panozd = 0;                                                      //  pre-align dialog

double   panPreAlignSize = 1000;                                           //  pre-align image size (ww)
double   panInitAlignSize = 200;                                           //  initial align image size
double   panImageIncrease = 1.6;                                           //  image size increase per align cycle
double   panSampSize = 10000;                                              //  pixel sample size

double   panPreAlignBlend = 0.30;                                          //  pre-align blend width * ww
double   panInitBlend = 0.20;                                              //  initial blend width during auto-align
double   panFinalBlend = 0.08;                                             //  final blend width * ww
double   panBlendDecrease = 0.8;                                           //  blend width reduction per align cycle

double   panInitSearchRange = 5.0;                                         //  initial search range, +/- pixels
double   panInitSearchStep = 0.7;                                          //  initial search step, pixels
double   panInitWarpRange = 4.0;                                           //  initial corner warp range, +/- pixels
double   panInitWarpStep = 1.0;                                            //  initial corner warp step, pixels
double   panSearchRange = 3.0;                                             //  normal search range, +/- pixels
double   panSearchStep = 1.0;                                              //  normal search step, pixels
double   panWarpRange = 2.0;                                               //  normal corner warp range, +/- pixels
double   panWarpStep = 1.0;                                                //  normal corner warp step, pixels

void  pano_prealign();                                                     //  manual pre-align
void  pano_align();                                                        //  auto fine-align
void  pano_tweak();                                                        //  user color tweak

editfunc    EFpano;                                                        //  edit function data


//  menu function

void m_pano(GtkWidget *, cchar *)                                          //  v.10.7
{
   int      imx, err;
   char     **flist = 0;

   zfuncs::F1_help_topic = "panorama";                                     //  help topic

   if (mod_keep()) return;                                                 //  warn unsaved changes
   if (! menulock(1)) return;                                              //  test menu lock            v.11.07
   menulock(0);

   for (imx = 0; imx < 10; imx++)
   {                                                                       //  clear all file and PXM data
      cimFile[imx] = 0;
      cimPXMf[imx] = cimPXMs[imx] = cimPXMw[imx] = 0;
   }
   cimNF = 0;   

   flist = zgetfileN(ZTX("Select 2 to 4 files"),"openN",curr_file);        //  select images to combine
   if (! flist) return;

   for (imx = 0; flist[imx]; imx++);                                       //  count selected files
   if (imx < 2 || imx > 4) {
      zmessageACK(mWin,ZTX("Select 2 to 4 files"));
      goto cleanup;
   }

   cimNF = imx;                                                            //  file count
   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = strdupz(flist[imx],0,"pano");                         //  set up file list
   
   if (! cim_load_files()) goto cleanup;                                   //  load and check all files

   free_resources();                                                       //  ready to commit

   err = f_open(cimFile[0],0);                                             //  curr_file = 1st file in list
   if (err) goto cleanup;

   EFpano.funcname = "pano";
   if (! edit_setup(EFpano)) goto cleanup;                                 //  setup edit (will lock)

   cimShowAll = 1;                                                         //  for cim_show_images(), show all    v.10.9
   cimShrink = 0;                                                          //  no warp shrinkage                  v.11.04
   cimPano = 1;                                                            //  horizontal pano mode               v.11.04
   cimPanoV = 0;
   
   pano_prealign();                                                        //  manual pre-alignment
   if (panStat != 1) goto cancel;

   pano_align();                                                           //  auto full alignment
   if (panStat != 1) goto cancel;

   pano_tweak();                                                           //  manual color adjustment
   if (panStat != 1) goto cancel;

   CEF->Fmod = 1;                                                          //  done
   edit_done(EFpano);
   goto cleanup;

cancel:                                                                    //  failed or canceled
   edit_cancel(EFpano);

cleanup:

   if (flist) {
      for (imx = 0; flist[imx]; imx++)                                     //  free file list
         zfree(flist[imx]);
      zfree(flist);
   }

   for (imx = 0; imx < cimNF; imx++) {                                     //  free cim file and PXM data
      if (cimFile[imx]) zfree(cimFile[imx]);
      if (cimPXMf[imx]) PXM_free(cimPXMf[imx]);
      if (cimPXMs[imx]) PXM_free(cimPXMs[imx]);
      if (cimPXMw[imx]) PXM_free(cimPXMw[imx]);
   }
   
   *SB_text = 0;
   return;
}


//  perform manual pre-align of all images
//  returns alignment data in cimOffs[*]
//  lens_mm and lens_bow may also be altered

void pano_prealign()                                                       //  v.10.7
{
   int    pano_prealign_event(zdialog *zd, cchar *event);                  //  dialog event function
   void * pano_prealign_thread(void *);                                    //  working thread

   int         imx, ww, err = 0;
   cchar       *exifkey = { exif_focal_length_key };
   cchar       *lensource;
   char        **pp = 0;

   cchar  *align_mess = ZTX("Drag images into rough alignment.\n"
                            "To rotate, drag from lower edge.");
   cchar  *search_mess = ZTX("Search for lens mm and bow");

   lens_bow = lens_settings[1];                                            //  no EXIF for this             v.12.01

   pp = info_get(curr_file,&exifkey,1);                                    //  get lens mm from EXIF if available
   if (pp && *pp) {
      err = convSD(*pp, lens_mm, 20, 1000);
      lensource = "(EXIF)";                                                //  lens mm from EXIF
   }

   if (! pp || ! *pp || err) {                                             //  not available
      lens_mm = lens_settings[0];                                          //                               v.12.01
      lensource = "(settings)";                                            //  lens mm from user settings
   }
   
   for (imx = 0; imx < 10; imx++)                                          //  set all alignment offsets = 0
      memset(&cimOffs[imx],0,sizeof(cimoffs));
      
   for (imx = ww = 0; imx < cimNF; imx++)                                  //  sum image widths
      ww += cimPXMf[imx]->ww;
   
   cimScale = 1.4 * panPreAlignSize / ww;                                  //  set alignment image scale
   if (cimScale > 1.0) cimScale = 1.0;                                     //  (* 0.7 after overlaps)

   for (imx = 0; imx < cimNF; imx++)                                       //  scale images > cimPXMs[*]
      cim_scale_image(imx,cimPXMs);

   for (imx = 0; imx < cimNF; imx++) {                                     //  curve images, cimPXMs[*] replaced
      cim_curve_image(imx);
      cimPXMw[imx] = PXM_copy(cimPXMs[imx]);                               //  copy to cimPXMw[*] for display
   }
   
   cimOffs[0].xf = cimOffs[0].yf = 0;                                      //  first image at (0,0)

   for (imx = 1; imx < cimNF; imx++)                                       //  position images with 30% overlap
   {                                                                       //    in horizontal row
      cimOffs[imx].xf = cimOffs[imx-1].xf + 0.7 * cimPXMw[imx-1]->ww;
      cimOffs[imx].yf = cimOffs[imx-1].yf;
   }
   
   Fzoom = 0;                                                              //  scale image to fit window
   Fblowup = 1;                                                            //  magnify small image to window size

   cimBlend = panPreAlignBlend * cimPXMw[1]->ww;                           //  overlap in align window
   cim_show_images(1,0);                                                   //  combine and show images in main window

   panozd = zdialog_new(ZTX("Pre-align Images"),mWin,Bproceed,Bcancel,null);     //  start pre-align dialog
   zdialog_add_widget(panozd,"label","lab1","dialog",align_mess,"space=5");
   zdialog_add_widget(panozd,"hbox","hb1","dialog",0,"space=2");
   zdialog_add_widget(panozd,"spin","spmm","hb1","20|999|0.1|35","space=5");     //  [ 35  ]    lens mm  (source)
   zdialog_add_widget(panozd,"label","labmm","hb1",ZTX("lens mm"));              //  [ 0.3 ]    lens bow
   zdialog_add_widget(panozd,"label","labsorc","hb1","","space=5");              //  [resize]   resize window
   zdialog_add_widget(panozd,"hbox","hb2","dialog",0,"space=2");                 //  [search]   search lens mm and bow
   zdialog_add_widget(panozd,"spin","spbow","hb2","-9|9|0.01|0","space=5");
   zdialog_add_widget(panozd,"label","labbow","hb2",ZTX("lens bow"));
   zdialog_add_widget(panozd,"hbox","hb3","dialog",0,"space=2");
   zdialog_add_widget(panozd,"button","resize","hb3",ZTX("Resize"),"space=5");
   zdialog_add_widget(panozd,"label","labsiz","hb3",ZTX("resize window"),"space=5");
   zdialog_add_widget(panozd,"hbox","hb4","dialog",0,"space=2");
   zdialog_add_widget(panozd,"button","search","hb4",Bsearch,"space=5");
   zdialog_add_widget(panozd,"label","labsearch","hb4",search_mess,"space=5");

   zdialog_stuff(panozd,"spmm",lens_mm);                                   //  stuff lens data
   zdialog_stuff(panozd,"spbow",lens_bow);
   zdialog_stuff(panozd,"labsorc",lensource);                              //  show source of lens data

   panStat = -1;                                                           //  busy status
   gdk_window_set_cursor(drWin->window,dragcursor);                        //  set drag cursor              v.11.03
   zdialog_run(panozd,pano_prealign_event,"-10/20");                       //  start dialog                 v.11.07
   start_thread(pano_prealign_thread,0);                                   //  start working thread
   zdialog_wait(panozd);                                                   //  wait for dialog completion
   gdk_window_set_cursor(drWin->window,0);                                 //  restore normal cursor        v.11.03
   Fzoom = Fblowup = 0;
   return;
}


//  pre-align dialog event function

int pano_prealign_event(zdialog *zd, cchar *event)                         //  v.10.7
{
   int      imx;
   double   overlap;

   if (strstr("spmm spbow",event)) {
      zdialog_fetch(zd,"spmm",lens_mm);                                    //  get revised lens data
      zdialog_fetch(zd,"spbow",lens_bow);
   }
   
   if (strEqu(event,"resize"))                                             //  allocate new E3 image
      cim_show_images(1,0);

   if (strEqu(event,"search")) {                                           //  search for optimal lens parms
      if (cimNF != 2)
         zmessageACK(mWin,ZTX("use two images only"));
      else  panStat = 2;                                                   //  tell thread to search
      return 0;
   }

   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1)                                                  //  proceed
         panStat = 1;
      else                                                                 //  cancel or other
         panStat = 0;

      zdialog_free(panozd);                                                //  kill dialog
      wrapup_thread(0);                                                    //  wait for thread
      
      if (! panStat) return 0;                                             //  canceled

      for (imx = 0; imx < cimNF-1; imx++)                                  //  check for enough overlap
      {
         overlap = cim_get_overlap(imx,imx+1,cimPXMs);                     //  v.11.04
         if (overlap < panFinalBlend) {                                    //  v.11.09
            zmessageACK(mWin,ZTX("Too little overlap, cannot align"));
            panStat = 0;
            return 0;
         }
      }
   }

   return 0;
}


//  pre-align working thread
//  convert mouse and KB events into image movements                       //  overhauled   v.11.02

void * pano_prealign_thread(void *)
{
   void   pano_autolens();

   cimoffs     offstemp;
   PXM         *pxmtemp;
   char        *ftemp;
   int         im1, im2, imm, imx;
   int         mx0, my0, mx, my;                                           //  mouse drag origin, position
   int         xoff, yoff, lox, hix;
   int         sepx, minsep;
   int         ww, hh, rotate, midx;
   double      lens_mm0, lens_bow0;
   double      dx, dy, t1, t2, dt;
      
   imm = ww = hh = rotate = xoff = yoff = 0;                               //  stop compiler warnings

   lens_mm0 = lens_mm;                                                     //  to detect changes
   lens_bow0 = lens_bow;

   mx0 = my0 = 0;                                                          //  no drag in progress
   Mcapture = KBcapture = 1;                                               //  capture mouse drag and KB keys

   cimBlend = 0;                                                           //  full blend during pre-align

   while (true)                                                            //  loop and align until done
   {
      zsleep(0.05);                                                        //  logic simplified
      
      if (panStat == 2) {                                                  //  dialog search button
         panStat = -1;                                                     //  back to busy status
         pano_autolens();
      }
      
      if (panStat != -1) break;                                            //  quit signal from dialog
      
      if (lens_mm != lens_mm0 || lens_bow != lens_bow0) {                  //  change in lens parameters
         lens_mm0 = lens_mm;
         lens_bow0 = lens_bow;

         for (imx = 0; imx < cimNF; imx++) {                               //  re-curve images
            cim_scale_image(imx,cimPXMs);
            cim_curve_image(imx);
            PXM_free(cimPXMw[imx]);
            cimPXMw[imx] = PXM_copy(cimPXMs[imx]);
         }

         cim_show_images(1,0);                                             //  combine and show images
         continue;
      }
      
      if (KBkey) {                                                         //  KB input
         if (KBkey == GDK_Left)  cimOffs[imm].xf -= 0.5;                   //  tweak alignment offsets
         if (KBkey == GDK_Right) cimOffs[imm].xf += 0.5;
         if (KBkey == GDK_Up)    cimOffs[imm].yf -= 0.5;
         if (KBkey == GDK_Down)  cimOffs[imm].yf += 0.5;
         if (KBkey == GDK_r)     cimOffs[imm].tf += 0.0005;
         if (KBkey == GDK_l)     cimOffs[imm].tf -= 0.0005;
         KBkey = 0;

         cim_show_images(0,0);                                             //  combine and show images
         continue;
      }

      if (! Mxdrag && ! Mydrag)                                            //  no drag underway
         mx0 = my0 = 0;                                                    //  reset drag origin

      if (Mxdrag || Mydrag)                                                //  mouse drag underway
      {
         mx = Mxdrag;                                                      //  mouse position in image
         my = Mydrag;

         if (! mx0 && ! my0)                                               //  new drag
         {
            mx0 = mx;                                                      //  set drag origin
            my0 = my;
            minsep = 9999;
            
            for (imx = 0; imx < cimNF; imx++)                              //  find image with midpoint
            {                                                              //    closest to mouse x
               lox = cimOffs[imx].xf;
               hix = lox + cimPXMw[imx]->ww;
               midx = (lox + hix) / 2;
               sepx = abs(midx - mx0);
               if (sepx < minsep) {
                  minsep = sepx;
                  imm = imx;                                               //  image to drag or rotate
               }
            }
            
            xoff = cimOffs[imm].xf;
            yoff = cimOffs[imm].yf;
            ww = cimPXMw[imm]->ww;
            hh = cimPXMw[imm]->hh;

            rotate = 0;                                                    //  if drag at bottom edge,
            if (my0 > yoff + 0.85 * hh) rotate = 1;                        //    set rotate flag         v.11.04
         }
         
         if (mx != mx0 || my != my0)                                       //  drag is progressing
         {
            dx = mx - mx0;                                                 //  mouse movement
            dy = my - my0;
            
            if (rotate && my0 > yoff && my > yoff)                         //  rotation
            {
               if (imm > 0) {
                  lox = cimOffs[imm].xf;                                   //  if there is an image to the left,
                  hix = cimOffs[imm-1].xf + cimPXMw[imm-1]->ww;            //    midx = midpoint of overlap
                  midx = (lox + hix) / 2;
               }
               else midx = 0;                                              //  this is the leftmost image

               t1 = atan(1.0 * (mx0-xoff) / (my0-yoff));
               t2 = atan(1.0 * (mx-xoff) / (my-yoff));
               dt = t1 - t2;                                               //  angle change
               dx = dt * (hh/2 + yoff);                                    //  pivot = middle of overlap on left
               dy = -dt * (midx-xoff);
            }

            else  dt = 0;                                                  //  x/y drag

            cimOffs[imm].xf += dx;                                         //  update image
            cimOffs[imm].yf += dy;
            cimOffs[imm].tf += dt;
            xoff = cimOffs[imm].xf;                                        //  v.11.04
            yoff = cimOffs[imm].yf;

            cim_show_images(0,0);                                          //  show combined images

            mx0 = mx;                                                      //  next drag origin = current mouse
            my0 = my;
         }
      }

      for (im1 = 0; im1 < cimNF-1; im1++)                                  //  track image order changes
      {
         im2 = im1 + 1;
         if (cimOffs[im2].xf < cimOffs[im1].xf) 
         {
            ftemp = cimFile[im2];                                          //  switch filespecs
            cimFile[im2] = cimFile[im1];
            cimFile[im1] = ftemp;
            pxmtemp = cimPXMf[im2];                                        //  switch images
            cimPXMf[im2] = cimPXMf[im1];
            cimPXMf[im1] = pxmtemp;
            pxmtemp = cimPXMs[im2];                                        //  scaled images
            cimPXMs[im2] = cimPXMs[im1];
            cimPXMs[im1] = pxmtemp;
            pxmtemp = cimPXMw[im2];                                        //  warped images
            cimPXMw[im2] = cimPXMw[im1];
            cimPXMw[im1] = pxmtemp;
            offstemp = cimOffs[im2];                                       //  offsets
            cimOffs[im2] = cimOffs[im1];
            cimOffs[im1] = offstemp;
            if (imm == im1) imm = im2;                                     //  current drag image
            else if (imm == im2) imm = im1;
            break;
         }
      }
   }
   
   KBcapture = Mcapture = 0;
   thread_exit();
   return 0;                                                               //  not executed, stop g++ warning
}


//  optimize lens parameters
//  inputs and outputs:
//     pre-aligned images cimPXMw[0] and [1]
//     offsets in cimOffs[0] and [1]
//     lens_mm, lens_bow

void pano_autolens()                                                       //  v.10.7
{
   double      mm_range, bow_range, xf_range, yf_range, tf_range;
   double      squeeze, xf_rfinal, rnum, matchB, matchlev;
   double      overlap, lens_mmB, lens_bowB;
   int         imx, randcount = 0;
   cimoffs     offsetsB;

   overlap = cim_get_overlap(0,1,cimPXMs);                                 //  v.11.04
   if (overlap < 0.1) {
      threadmessage = ZTX("Too little overlap, cannot align");
      return;
   }

   Ffuncbusy++;                                                            //  v.11.01

   cimSampSize = 2000;                                                     //  v.11.03
   cimNsearch = 0;

   mm_range = 0.1 * lens_mm;                                               //  set initial search ranges    v.11.03
   bow_range = 0.3 * lens_bow;
   if (bow_range < 0.5) bow_range = 0.5;
   xf_range = 7;
   yf_range = 7;
   tf_range = 0.01;
   xf_rfinal = 0.3;                                                        //  final xf range - when to quit

   cim_match_colors(0,1,cimPXMw);                                          //  adjust colors for image matching
   cim_adjust_colors(cimPXMs[0],1);
   cim_adjust_colors(cimPXMw[0],1);
   cim_adjust_colors(cimPXMs[1],2);
   cim_adjust_colors(cimPXMw[1],2);

   lens_mmB = lens_mm;                                                     //  starting point
   lens_bowB = lens_bow;
   offsetsB = cimOffs[1];
   cimSearchRange = 7;

   matchB = 0;

   while (true)
   {
      srand48(time(0) + randcount++);
      lens_mm = lens_mmB + mm_range * (drand48() - 0.5);                   //  new random lens factors
      lens_bow = lens_bowB + bow_range * (drand48() - 0.5);                //     within search range
      
      for (imx = 0; imx <= 1; imx++) {                                     //  re-curve images
         cim_scale_image(imx,cimPXMs);
         cim_curve_image(imx);
         PXM_free(cimPXMw[imx]);
         cimPXMw[imx] = PXM_copy(cimPXMs[imx]);
      }

      cim_get_redpix(0);                                                   //  get high-contrast pixels     v.11.03
      cim_show_images(0,0);                                                //  combine and show images

      squeeze = 0.97;                                                      //  search range reduction       v.10.7
         
      for (int ii = 0; ii < 1000; ii++)                                    //  loop random x/y/t alignments
      {                                                                    
         rnum = drand48();
         if (rnum < 0.33)                                                  //  random change some alignment offset 
            cimOffs[1].xf = offsetsB.xf + xf_range * (drand48() - 0.5);
         else if (rnum < 0.67)
            cimOffs[1].yf = offsetsB.yf + yf_range * (drand48() - 0.5);
         else
            cimOffs[1].tf = offsetsB.tf + tf_range * (drand48() - 0.5);

         matchlev = cim_match_images(0,1);                                 //  test quality of image alignment

         sprintf(SB_text,"align: %d  match: %.5f  lens: %.1f %.2f",        //  update status bar
                           ++cimNsearch, matchB, lens_mmB, lens_bowB);
         zmainloop();                                                      //  v.11.11.1

         if (sigdiff(matchlev,matchB,0.00001) > 0) {
            matchB = matchlev;                                             //  save new best fit
            lens_mmB = lens_mm;                                            //  alignment is better
            lens_bowB = lens_bow;
            offsetsB = cimOffs[1];
            cim_show_images(0,0);
            squeeze = 1;                                                   //  keep same search range as long
            break;                                                         //    as improvements are found
         }

         if (panStat != -1) goto done;                                     //  user kill
      }
      
      if (xf_range < xf_rfinal) goto done;                                 //  finished

      sprintf(SB_text,"align: %d  match: %.5f  lens: %.1f %.2f",           //  update status bar
                        cimNsearch, matchB, lens_mmB, lens_bowB);
      zmainloop();                                                         //  v.11.11.1

      mm_range = squeeze * mm_range;                                       //  reduce search range if no 
      if (mm_range < 0.02 * lens_mmB) mm_range = 0.02 * lens_mmB;          //    improvements were found
      bow_range = squeeze * bow_range;
      if (bow_range < 0.1 * lens_bowB) bow_range = 0.1 * lens_bowB;
      if (bow_range < 0.2) bow_range = 0.2;
      xf_range = squeeze * xf_range;
      yf_range = squeeze * yf_range;
      tf_range = squeeze * tf_range;
   }

done:
   zfree(cimRedpix);
   cimRedpix = 0;

   lens_mm = lens_mmB;                                                     //  save best lens params found
   lens_bow = lens_bowB;
   if (panStat == -1 && panozd) {                                          //  unless killed 
      zdialog_stuff(panozd,"spmm",lens_mm);
      zdialog_stuff(panozd,"spbow",lens_bow);
   }

   cimSampSize = panSampSize;                                              //  restore
   Ffuncbusy--;
   cim_show_images(1,0);                                                   //  images are left color-matched
   return;
}


//  fine-alignment
//  start with very small image size
//  search around offset values for best match
//  increase image size and loop until full-size

void pano_align()                                                          //  v.10.7
{
   int         imx, im1, im2, ww;
   double      R, dx, dy, dt;
   double      overlap;
   cimoffs     offsets0;
   
   Fzoom = 0;                                                              //  scale E3 to fit window
   Fblowup = 1;                                                            //  magnify small image to window size
   Ffuncbusy++;                                                            //  v.11.01

   for (imx = 0; imx < cimNF; imx++) {
      cimOffs[imx].xf = cimOffs[imx].xf / cimScale;                        //  scale x/y offsets for full-size images
      cimOffs[imx].yf = cimOffs[imx].yf / cimScale;
   }

   cimScale = 1.0;                                                         //  full-size

   for (imx = 0; imx < cimNF; imx++) {
      PXM_free(cimPXMs[imx]);
      cimPXMs[imx] = PXM_copy(cimPXMf[imx]);                               //  copy full-size images
      cim_curve_image(imx);                                                //  curve them
   }

   cimBlend = 0.3 * cimPXMs[0]->ww;
   cim_get_overlap(0,1,cimPXMs);                                           //  match images 0 & 1 in overlap area
   cim_match_colors(0,1,cimPXMs);
   cim_adjust_colors(cimPXMf[0],1);                                        //  image 0 << profile 1
   cim_adjust_colors(cimPXMf[1],2);                                        //  image 1 << profile 2

   if (cimNF > 2) {
      cimBlend = 0.3 * cimPXMs[1]->ww;
      cim_get_overlap(1,2,cimPXMs);
      cim_match_colors(1,2,cimPXMs);
      cim_adjust_colors(cimPXMf[0],1);
      cim_adjust_colors(cimPXMf[1],1);
      cim_adjust_colors(cimPXMf[2],2);
   }

   if (cimNF > 3) {
      cimBlend = 0.3 * cimPXMs[2]->ww;
      cim_get_overlap(2,3,cimPXMs);
      cim_match_colors(2,3,cimPXMs);
      cim_adjust_colors(cimPXMf[0],1);
      cim_adjust_colors(cimPXMf[1],1);
      cim_adjust_colors(cimPXMf[2],1);
      cim_adjust_colors(cimPXMf[3],2);
   }

   cimScale = panInitAlignSize / cimPXMf[1]->hh;                           //  initial align image scale
   if (cimScale > 1.0) cimScale = 1.0;

   for (imx = 0; imx < cimNF; imx++) {                                     //  scale offsets for image scale
      cimOffs[imx].xf = cimOffs[imx].xf * cimScale;
      cimOffs[imx].yf = cimOffs[imx].yf * cimScale;
   }
   
   cimSearchRange = panInitSearchRange;                                    //  initial align search range
   cimSearchStep = panInitSearchStep;                                      //  initial align search step
   cimWarpRange = panInitWarpRange;                                        //  initial align corner warp range
   cimWarpStep = panInitWarpStep;                                          //  initial align corner warp step
   ww = cimPXMf[0]->ww * cimScale;                                         //  initial align image width
   cimBlend = ww * panInitBlend;                                           //  initial align blend width
   cimSampSize = panSampSize;                                              //  pixel sample size for align/compare
   cimNsearch = 0;                                                         //  reset align search counter
   
   while (true)                                                            //  loop, increasing image size
   {
      for (imx = 0; imx < cimNF; imx++) {                                  //  prepare images
         cim_scale_image(imx,cimPXMs);                                     //  scale to new size
         cim_curve_image(imx);                                             //  curve based on lens params
         cim_warp_image_pano(imx,1);                                       //  apply corner warps
      }

      cim_show_images(1,0);                                                //  show with 50/50 blend in overlaps

      for (im1 = 0; im1 < cimNF-1; im1++)                                  //  fine-align each image with left neighbor
      {
         im2 = im1 + 1;

         offsets0 = cimOffs[im2];                                          //  save initial alignment offsets
         overlap = cim_get_overlap(im1,im2,cimPXMs);                       //  get overlap area          v.11.04
         if (overlap < panFinalBlend-2) {
            zmessageACK(mWin,ZTX("Too little overlap, cannot align"));     //  v.11.03
            goto fail;
         }
         cim_get_redpix(im1);                                              //  get high-contrast pixels
         
         cim_align_image(im1,im2);                                         //  search for best offsets and warps

         zfree(cimRedpix);                                                 //  clear red pixels
         cimRedpix = 0;

         dx = cimOffs[im2].xf - offsets0.xf;                               //  changes from initial offsets
         dy = cimOffs[im2].yf - offsets0.yf;
         dt = cimOffs[im2].tf - offsets0.tf;
         
         for (imx = im2+1; imx < cimNF; imx++)                             //  propagate to following images
         {
            cimOffs[imx].xf += dx;
            cimOffs[imx].yf += dy;
            cimOffs[imx].tf += dt;
            ww = cimOffs[imx].xf - cimOffs[im2].xf;
            cimOffs[imx].yf += ww * dt;
         }
      }

      if (cimScale == 1.0) goto success;                                   //  done

      R = panImageIncrease;                                                //  next larger image size
      cimScale = cimScale * R;
      if (cimScale > 0.85) {                                               //  if close to end, jump to end
         R = R / cimScale;
         cimScale = 1.0;
      }

      for (imx = 0; imx < cimNF; imx++)                                    //  scale offsets for new size
      {
         cimOffs[imx].xf *= R;
         cimOffs[imx].yf *= R;

         for (int ii = 0; ii < 4; ii++) {
            cimOffs[imx].wx[ii] *= R;
            cimOffs[imx].wy[ii] *= R;
         }
      }

      cimSearchRange = panSearchRange;                                     //  align search range
      cimSearchStep = panSearchStep;                                       //  align search step size
      cimWarpRange = panWarpRange;                                         //  align corner warp range
      cimWarpStep = panWarpStep;                                           //  align corner warp step size

      cimBlend = cimBlend * panBlendDecrease * R;                          //  blend width, reduced
      ww = cimPXMf[0]->ww * cimScale;
      if (cimBlend < panFinalBlend * ww) 
         cimBlend = panFinalBlend * ww;                                    //  stay above minimum
   }

success: 
   panStat = 1;
   goto align_done;
fail:
   panStat = 0;
align_done:
   cimBlend = 1;                                                           //  tiny blend (increase in tweak if wanted)
   Fzoom = Fblowup = 0;
   Ffuncbusy--;
   cim_show_images(0,0);
   return;
}


//  get user inputs for RGB changes and blend width, update cimPXMw[*]

void pano_tweak()                                                          //  v.10.7
{
   int    pano_tweak_event(zdialog *zd, cchar *event);                     //  dialog event function
   
   cchar    *tweaktitle = ZTX("Match Brightness and Color");
   char     imageN[8] = "imageN";
   int      imx;
   
   cimBlend = 1;                                                           //  init. blend width
   
   panozd = zdialog_new(tweaktitle,mWin,Bdone,Bcancel,null);

   zdialog_add_widget(panozd,"hbox","hbim","dialog",0,"space=5");
   zdialog_add_widget(panozd,"label","labim","hbim",ZTX("image"),"space=5");      //  image  (o)  (o)  (o)  (o)
   zdialog_add_widget(panozd,"hbox","hbc1","dialog",0,"homog");                   //
   zdialog_add_widget(panozd,"label","labred","hbc1",Bred);                       //     red     green    blue
   zdialog_add_widget(panozd,"label","labgreen","hbc1",Bgreen);                   //   [_____]  [_____]  [_____]
   zdialog_add_widget(panozd,"label","labblue","hbc1",Bblue);                     //
   zdialog_add_widget(panozd,"hbox","hbc2","dialog",0,"homog");                   //   brightness [___]  [apply]
   zdialog_add_widget(panozd,"spin","red","hbc2","50|200|0.1|100","space=5");     //
   zdialog_add_widget(panozd,"spin","green","hbc2","50|200|0.1|100","space=5");   //   --------------------------
   zdialog_add_widget(panozd,"spin","blue","hbc2","50|200|0.1|100","space=5");    //
   zdialog_add_widget(panozd,"hbox","hbbri","dialog",0,"space=5");                //   [auto color]  [file color]
   zdialog_add_widget(panozd,"label","labbr","hbbri",Bbrightness,"space=5");      //
   zdialog_add_widget(panozd,"spin","bright","hbbri","50|200|0.1|100");           //   --------------------------
   zdialog_add_widget(panozd,"button","brapp","hbbri",Bapply,"space=10");         //
   zdialog_add_widget(panozd,"hsep","hsep","dialog",0,"space=5");                 //   blend width [___] [apply]   
   zdialog_add_widget(panozd,"hbox","hbc3","dialog",0,"space=5");                 //
   zdialog_add_widget(panozd,"button","auto","hbc3",ZTX("auto color"),"space=5"); //          [done]  [cancel]
   zdialog_add_widget(panozd,"button","file","hbc3",ZTX("file color"),"space=5");
   zdialog_add_widget(panozd,"hsep","hsep","dialog",0,"space=5");
   zdialog_add_widget(panozd,"hbox","hbblen","dialog",0);
   zdialog_add_widget(panozd,"label","labbl","hbblen",Bblendwidth,"space=5");
   zdialog_add_widget(panozd,"spin","blend","hbblen","1|300|1|1");
   zdialog_add_widget(panozd,"button","blapp","hbblen",Bapply,"space=15");
   
   for (imx = 0; imx < cimNF; imx++) {                                     //  add radio button per image
      imageN[5] = '0' + imx;
      zdialog_add_widget(panozd,"radio",imageN,"hbim",0,"space=5");
   }
   
   zdialog_stuff(panozd,"image0",1);                                       //  pre-select 1st image
   zdialog_resize(panozd,300,0);

   panStat = -1;                                                           //  busy status
   zdialog_run(panozd,pano_tweak_event,"-10/20");                          //  run dialog, parallel            v.11.07
   zdialog_wait(panozd);                                                   //  wait for dialog completion
   return;
}


//  dialog event function

int pano_tweak_event(zdialog *zd, cchar *event)                            //  v.10.7
{
   char        imageN[8] = "imageN";
   double      red, green, blue, bright, bright2;
   double      red1, green1, blue1;
   int         nn, im0, imx, im1, im2, ww, hh, px, py;
   uint16      *pixel;
   
   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) panStat = 1;                                     //  done
      if (zd->zstat == 2) panStat = 0;                                     //  cancel
      zdialog_free(panozd);                                                //  kill dialog
      return 0;
   }

   for (im0 = 0; im0 < cimNF; im0++) {                                     //  get which image is selected
      imageN[5] = '0' + im0;                                               //    by the radio buttons
      zdialog_fetch(zd,imageN,nn);
      if (nn) break;
   }
   if (im0 == cimNF) return 1;

   zdialog_fetch(zd,"red",red);                                            //  get color adjustments
   zdialog_fetch(zd,"green",green);
   zdialog_fetch(zd,"blue",blue);
   zdialog_fetch(zd,"bright",bright);                                      //  brightness adjustment

   bright2 = (red + green + blue) / 3;                                     //  RGB brightness
   bright = bright / bright2;                                              //  bright setpoint / RGB brightness
   red = red * bright;                                                     //  adjust RGB brightness
   green = green * bright;
   blue = blue * bright;
   
   bright = (red + green + blue) / 3;
   zdialog_stuff(zd,"red",red);                                            //  force back into consistency
   zdialog_stuff(zd,"green",green);
   zdialog_stuff(zd,"blue",blue);
   zdialog_stuff(zd,"bright",bright);
   
   if (strEqu(event,"brapp"))                                              //  apply color & brightness changes
   {
      red = red / 100;                                                     //  normalize 0.5 ... 2.0
      green = green / 100;
      blue = blue / 100;
      
      cim_warp_image_pano(im0,0);                                          //  refresh cimPXMw from cimPXMs

      ww = cimPXMw[im0]->ww;
      hh = cimPXMw[im0]->hh;
      
      for (py = 0; py < hh; py++)                                          //  loop all image pixels
      for (px = 0; px < ww; px++)
      {
         pixel = PXMpix(cimPXMw[im0],px,py);
         red1 = red * pixel[0];                                            //  apply color factors
         green1 = green * pixel[1];
         blue1 = blue * pixel[2];
         if (! blue1) continue;

         if (red1 > 65535 || green1 > 65535 || blue1 > 65535) {
            bright = red1;                                                 //  avoid overflow
            if (green1 > bright) bright = green1;
            if (blue1 > bright) bright = blue1;
            bright = 65535.0 / bright;
            red1 = red1 * bright;
            green1 = green1 * bright;
            blue1 = blue1 * bright;
         }
         
         if (blue1 < 1) blue1 = 1;                                         //  avoid 0       v.10.7

         pixel[0] = red1;
         pixel[1] = green1;
         pixel[2] = blue1;
      }
      
      cimBlend = 1;
      zdialog_stuff(zd,"blend",cimBlend);                                  //  v.11.04
      cim_show_images(0,0);                                                //  combine and show with 50/50 blend
   }
   
   if (strEqu(event,"auto"))                                               //  auto match color of selected image
   {
      for (im1 = im0; im1 < cimNF-1; im1++)                                //  from selected image to last image
      {
         im2 = im1 + 1;
         cimBlend = 0.3 * cimPXMw[im2]->ww;
         cim_get_overlap(im1,im2,cimPXMw);                                 //  match images in overlap area
         cim_match_colors(im1,im2,cimPXMw);
         cim_adjust_colors(cimPXMw[im1],1);                                //  image im1 << profile 1
         cim_adjust_colors(cimPXMw[im2],2);                                //  image im2 << profile 2
         for (imx = im1-1; imx >= im0; imx--)
            cim_adjust_colors(cimPXMw[imx],1);
         cimBlend = 1;
         zdialog_stuff(zd,"blend",cimBlend);                               //  v.11.04
         cim_show_images(0,0);
      }

      for (im1 = im0-1; im1 >= 0; im1--)                                   //  from selected image to 1st image
      {
         im2 = im1 + 1;
         cimBlend = 0.3 * cimPXMw[im2]->ww;
         cim_get_overlap(im1,im2,cimPXMw);                                 //  match images in overlap area
         cim_match_colors(im1,im2,cimPXMw);
         cim_adjust_colors(cimPXMw[im1],1);                                //  image im1 << profile 1
         cim_adjust_colors(cimPXMw[im2],2);                                //  image im2 << profile 2
         for (imx = im2+1; imx < cimNF; imx++)
            cim_adjust_colors(cimPXMw[imx],2);
         cimBlend = 1;
         zdialog_stuff(zd,"blend",cimBlend);                               //  v.11.04
         cim_show_images(0,0);
      }
   }
   
   if (strEqu(event,"file"))                                               //  use original file colors
   {
      if (! cim_load_files()) return 1;

      for (imx = 0; imx < cimNF; imx++) {
         PXM_free(cimPXMs[imx]);
         cimPXMs[imx] = PXM_copy(cimPXMf[imx]);
         cim_curve_image(imx);                                             //  curve and warp
         cim_warp_image_pano(imx,0);
      }

      cimBlend = 1;
      zdialog_stuff(zd,"blend",cimBlend);                                  //  v.11.04
      cim_show_images(0,0);
   }
   
   if (strEqu(event,"blapp"))                                              //  apply new blend width
   {
      zdialog_fetch(zd,"blend",cimBlend);                                  //  can be zero
      cim_show_images(0,1);                                                //  show with gradual blend
   }

   return 1;
}


/**************************************************************************

    Vertical Panorama function: join 2, 3, or 4 images.

***************************************************************************/

void  vpano_prealign();                                                    //  manual pre-align
void  vpano_align();                                                       //  auto fine-align
void  vpano_tweak();                                                       //  user color tweak

editfunc    EFvpano;                                                       //  edit function data


//  menu function

void m_vpano(GtkWidget *, cchar *)                                         //  v.11.04
{
   int      imx, err;
   char     **flist = 0;

   zfuncs::F1_help_topic = "panorama";                                     //  help topic

   if (mod_keep()) return;                                                 //  warn unsaved changes
   if (! menulock(1)) return;                                              //  test menu lock            v.11.07
   menulock(0);

   for (imx = 0; imx < 10; imx++)
   {                                                                       //  clear all file and PXM data
      cimFile[imx] = 0;
      cimPXMf[imx] = cimPXMs[imx] = cimPXMw[imx] = 0;
   }
   cimNF = 0;   

   flist = zgetfileN(ZTX("Select 2 to 4 files"),"openN",curr_file);        //  select images to combine
   if (! flist) return;

   for (imx = 0; flist[imx]; imx++);                                       //  count selected files
   if (imx < 2 || imx > 4) {
      zmessageACK(mWin,ZTX("Select 2 to 4 files"));
      goto cleanup;
   }

   cimNF = imx;                                                            //  file count
   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = strdupz(flist[imx],0,"pano");                         //  set up file list
   
   if (! cim_load_files()) goto cleanup;                                   //  load and check all files

   free_resources();                                                       //  ready to commit

   err = f_open(cimFile[0],0);                                             //  curr_file = 1st file in list
   if (err) goto cleanup;

   EFvpano.funcname = "vpano";
   if (! edit_setup(EFvpano)) goto cleanup;                                //  setup edit (will lock)

   cimShowAll = 1;                                                         //  for cim_show_images(), show all
   cimShrink = 0;                                                          //  no warp shrinkage                  v.11.04
   cimPano = 0;                                                            //  vertical pano mode                 v.11.04
   cimPanoV = 1;

   vpano_prealign();                                                       //  manual pre-alignment
   if (panStat != 1) goto cancel;

   vpano_align();                                                          //  auto full alignment
   if (panStat != 1) goto cancel;

   vpano_tweak();                                                          //  manual color adjustment
   if (panStat != 1) goto cancel;

   CEF->Fmod = 1;                                                          //  done
   edit_done(EFvpano);
   goto cleanup;

cancel:                                                                    //  failed or canceled
   edit_cancel(EFvpano);

cleanup:

   if (flist) {
      for (imx = 0; flist[imx]; imx++)                                     //  free file list
         zfree(flist[imx]);
      zfree(flist);
   }

   for (imx = 0; imx < cimNF; imx++) {                                     //  free cim file and PXM data
      if (cimFile[imx]) zfree(cimFile[imx]);
      if (cimPXMf[imx]) PXM_free(cimPXMf[imx]);
      if (cimPXMs[imx]) PXM_free(cimPXMs[imx]);
      if (cimPXMw[imx]) PXM_free(cimPXMw[imx]);
   }
   
   *SB_text = 0;
   return;
}


//  perform manual pre-align of all images
//  returns alignment data in cimOffs[*]
//  lens_mm and lens_bow may also be altered

void vpano_prealign()
{
   int    vpano_prealign_event(zdialog *zd, cchar *event);                 //  dialog event function
   void * vpano_prealign_thread(void *);                                   //  working thread

   int         imx, hh, err = 0;
   cchar       *exifkey = { exif_focal_length_key };
   cchar       *lensource;
   char        **pp = 0;

   cchar  *align_mess = ZTX("Drag images into rough alignment.\n"
                            "To rotate, drag from right edge.");

   lens_bow = lens_settings[1];                                            //  no EXIF for this

   pp = info_get(curr_file,&exifkey,1);                                    //  get lens mm from EXIF if available
   if (pp && *pp) {
      err = convSD(*pp, lens_mm, 20, 1000);
      lensource = "(EXIF)";                                                //  lens mm from EXIF
   }

   if (! pp || ! *pp || err) {                                             //  not available
      lens_mm = lens_settings[0];
      lensource = "(settings)";                                            //  lens mm from user settings
   }

   for (imx = 0; imx < 10; imx++)                                          //  set all alignment offsets = 0
      memset(&cimOffs[imx],0,sizeof(cimoffs));
      
   for (imx = hh = 0; imx < cimNF; imx++)                                  //  sum image heights
      hh += cimPXMf[imx]->hh;
   
   cimScale = 1.4 * panPreAlignSize / hh;                                  //  set alignment image scale
   if (cimScale > 1.0) cimScale = 1.0;                                     //  (* 0.7 after overlaps)

   for (imx = 0; imx < cimNF; imx++)                                       //  scale images > cimPXMs[*]
      cim_scale_image(imx,cimPXMs);

   for (imx = 0; imx < cimNF; imx++) {                                     //  curve images, cimPXMs[*] replaced
      cim_curve_Vimage(imx);
      cimPXMw[imx] = PXM_copy(cimPXMs[imx]);                               //  copy to cimPXMw[*] for display
   }
   
   cimOffs[0].xf = cimOffs[0].yf = 0;                                      //  first image at (0,0)

   for (imx = 1; imx < cimNF; imx++)                                       //  position images with 30% overlap
   {                                                                       //    in vertical row
      cimOffs[imx].yf = cimOffs[imx-1].yf + 0.7 * cimPXMw[imx-1]->hh;
      cimOffs[imx].xf = cimOffs[imx-1].xf;
   }
   
   Fzoom = 0;                                                              //  scale image to fit window
   Fblowup = 1;                                                            //  magnify small image to window size

   cimBlend = panPreAlignBlend * cimPXMw[1]->hh;                           //  overlap in align window
   cim_show_Vimages(1,0);                                                  //  combine and show images in main window

   panozd = zdialog_new(ZTX("Pre-align Images"),mWin,Bproceed,Bcancel,null);     //  start pre-align dialog
   zdialog_add_widget(panozd,"label","lab1","dialog",align_mess,"space=5");
   zdialog_add_widget(panozd,"hbox","hb1","dialog",0,"space=2");
   zdialog_add_widget(panozd,"spin","spmm","hb1","20|999|0.1|35","space=5");     //  [ 35  ]    lens mm  (source)
   zdialog_add_widget(panozd,"label","labmm","hb1",ZTX("lens mm"));              //  [ 0.3 ]    lens bow
   zdialog_add_widget(panozd,"label","labsorc","hb1","","space=5");              //  [resize]   resize window
   zdialog_add_widget(panozd,"hbox","hb2","dialog",0,"space=2");
   zdialog_add_widget(panozd,"spin","spbow","hb2","-9|9|0.01|0","space=5");
   zdialog_add_widget(panozd,"label","labbow","hb2",ZTX("lens bow"));
   zdialog_add_widget(panozd,"hbox","hb3","dialog",0,"space=2");
   zdialog_add_widget(panozd,"button","resize","hb3",ZTX("Resize"),"space=5");
   zdialog_add_widget(panozd,"label","labsiz","hb3",ZTX("resize window"),"space=5");

   zdialog_stuff(panozd,"spmm",lens_mm);                                   //  stuff lens data
   zdialog_stuff(panozd,"spbow",lens_bow);
   zdialog_stuff(panozd,"labsorc",lensource);                              //  show source of lens data

   panStat = -1;                                                           //  busy status
   gdk_window_set_cursor(drWin->window,dragcursor);                        //  set drag cursor 
   zdialog_run(panozd,vpano_prealign_event,"-10/20");                      //  start dialog              v.11.07
   start_thread(vpano_prealign_thread,0);                                  //  start working thread
   zdialog_wait(panozd);                                                   //  wait for dialog completion
   gdk_window_set_cursor(drWin->window,0);                                 //  restore normal cursor
   Fzoom = Fblowup = 0;
   return;
}


//  pre-align dialog event function

int vpano_prealign_event(zdialog *zd, cchar *event)
{
   int      imx;
   double   overlap;

   if (strstr("spmm spbow",event)) {
      zdialog_fetch(zd,"spmm",lens_mm);                                    //  get revised lens data
      zdialog_fetch(zd,"spbow",lens_bow);
   }
   
   if (strEqu(event,"resize"))                                             //  allocate new E3 image
      cim_show_Vimages(1,0);

   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1)                                                  //  proceed
         panStat = 1;
      else                                                                 //  cancel or other
         panStat = 0;

      zdialog_free(panozd);                                                //  kill dialog
      wrapup_thread(0);                                                    //  wait for thread
      
      if (! panStat) return 0;                                             //  canceled

      for (imx = 0; imx < cimNF-1; imx++)                                  //  check for enough overlap
      {
         overlap = cim_get_overlap(imx,imx+1,cimPXMs);                     //  v.11.04
         if (overlap < panFinalBlend) {                                    //  v.11.09
            zmessageACK(mWin,ZTX("Too little overlap, cannot align"));
            panStat = 0;
            return 0;
         }
      }
   }

   return 0;
}


//  pre-align working thread
//  convert mouse and KB events into image movements                       //  overhauled

void * vpano_prealign_thread(void *)
{
   cimoffs     offstemp;
   PXM         *pxmtemp;
   char        *ftemp;
   int         im1, im2, imm, imx;
   int         mx0, my0, mx, my;                                           //  mouse drag origin, position
   int         xoff, yoff, loy, hiy;
   int         sepy, minsep;
   int         ww, hh, rotate, midy;
   double      lens_mm0, lens_bow0;
   double      dx, dy, t1, t2, dt;
      
   imm = ww = hh = rotate = xoff = yoff = 0;                               //  stop compiler warnings

   lens_mm0 = lens_mm;                                                     //  to detect changes
   lens_bow0 = lens_bow;

   mx0 = my0 = 0;                                                          //  no drag in progress
   Mcapture = KBcapture = 1;                                               //  capture mouse drag and KB keys

   cimBlend = 0;                                                           //  full blend during pre-align

   while (true)                                                            //  loop and align until done
   {
      zsleep(0.05);                                                        //  logic simplified
      
      if (panStat != -1) break;                                            //  quit signal from dialog
      
      if (lens_mm != lens_mm0 || lens_bow != lens_bow0) {                  //  change in lens parameters
         lens_mm0 = lens_mm;
         lens_bow0 = lens_bow;

         for (imx = 0; imx < cimNF; imx++) {                               //  re-curve images
            cim_scale_image(imx,cimPXMs);
            cim_curve_Vimage(imx);
            PXM_free(cimPXMw[imx]);
            cimPXMw[imx] = PXM_copy(cimPXMs[imx]);
         }

         cim_show_Vimages(1,0);                                            //  combine and show images
         continue;
      }
      
      if (KBkey) {                                                         //  KB input
         if (KBkey == GDK_Left)  cimOffs[imm].xf -= 0.5;                   //  tweak alignment offsets
         if (KBkey == GDK_Right) cimOffs[imm].xf += 0.5;
         if (KBkey == GDK_Up)    cimOffs[imm].yf -= 0.5;
         if (KBkey == GDK_Down)  cimOffs[imm].yf += 0.5;
         if (KBkey == GDK_r)     cimOffs[imm].tf += 0.0005;
         if (KBkey == GDK_l)     cimOffs[imm].tf -= 0.0005;
         KBkey = 0;

         cim_show_Vimages(0,0);                                            //  combine and show images
         continue;
      }

      if (! Mxdrag && ! Mydrag)                                            //  no drag underway
         mx0 = my0 = 0;                                                    //  reset drag origin

      if (Mxdrag || Mydrag)                                                //  mouse drag underway
      {
         mx = Mxdrag;                                                      //  mouse position in image
         my = Mydrag;

         if (! mx0 && ! my0)                                               //  new drag
         {
            mx0 = mx;                                                      //  set drag origin
            my0 = my;
            minsep = 9999;
            
            for (imx = 0; imx < cimNF; imx++)                              //  find image with midpoint
            {                                                              //    closest to mouse y
               loy = cimOffs[imx].yf;
               hiy = loy + cimPXMw[imx]->hh;
               midy = (loy + hiy) / 2;
               sepy = abs(midy - my0);
               if (sepy < minsep) {
                  minsep = sepy;
                  imm = imx;                                               //  image to drag or rotate
               }
            }

            xoff = cimOffs[imm].xf;
            yoff = cimOffs[imm].yf;
            ww = cimPXMw[imm]->ww;
            hh = cimPXMw[imm]->hh;

            rotate = 0;                                                    //  if drag at right edge,
            if (mx0 > xoff + 0.85 * ww) rotate = 1;                        //    set rotate flag
         }
         
         if (mx != mx0 || my != my0)                                       //  drag is progressing
         {
            dx = mx - mx0;                                                 //  mouse movement
            dy = my - my0;
            
            if (rotate && my0 > yoff && my > yoff)                         //  rotation
            {
               if (imm > 0) {
                  loy = cimOffs[imm].yf;                                   //  if there is an image above,
                  hiy = cimOffs[imm-1].yf + cimPXMw[imm-1]->hh;            //    midy = midpoint of overlap
                  midy = (loy + hiy) / 2;
               }
               else midy = 0;                                              //  this is the topmist image
               
               t1 = atan(1.0 * (my0-yoff) / (mx0-xoff));
               t2 = atan(1.0 * (my-yoff) / (mx-xoff));
               dt = t2 - t1;                                               //  angle change
               dy = - dt * ww / 2;                                         //  pivot = middle of overlap above
               dx = dt * (midy-yoff);
            }

            else  dt = 0;                                                  //  x/y drag

            cimOffs[imm].xf += dx;                                         //  update image
            cimOffs[imm].yf += dy;
            cimOffs[imm].tf += dt;
            xoff = cimOffs[imm].xf;                                        //  v.11.04
            yoff = cimOffs[imm].yf;

            cim_show_Vimages(0,0);                                         //  show combined images

            mx0 = mx;                                                      //  next drag origin = current mouse
            my0 = my;
         }
      }

      for (im1 = 0; im1 < cimNF-1; im1++)                                  //  track image order changes
      {
         im2 = im1 + 1;
         if (cimOffs[im2].yf < cimOffs[im1].yf) 
         {
            ftemp = cimFile[im2];                                          //  switch filespecs
            cimFile[im2] = cimFile[im1];
            cimFile[im1] = ftemp;
            pxmtemp = cimPXMf[im2];                                        //  switch images
            cimPXMf[im2] = cimPXMf[im1];
            cimPXMf[im1] = pxmtemp;
            pxmtemp = cimPXMs[im2];                                        //  scaled images
            cimPXMs[im2] = cimPXMs[im1];
            cimPXMs[im1] = pxmtemp;
            pxmtemp = cimPXMw[im2];                                        //  warped images
            cimPXMw[im2] = cimPXMw[im1];
            cimPXMw[im1] = pxmtemp;
            offstemp = cimOffs[im2];                                       //  offsets
            cimOffs[im2] = cimOffs[im1];
            cimOffs[im1] = offstemp;
            if (imm == im1) imm = im2;                                     //  current drag image
            else if (imm == im2) imm = im1;
            break;
         }
      }
   }
   
   KBcapture = Mcapture = 0;
   thread_exit();
   return 0;                                                               //  not executed, stop g++ warning
}


//  fine-alignment
//  start with very small image size
//  search around offset values for best match
//  increase image size and loop until full-size

void vpano_align()
{
   int         imx, im1, im2, ww, hh;
   double      R, dx, dy, dt;
   double      overlap;
   cimoffs     offsets0;
   
   Fzoom = 0;                                                              //  scale E3 to fit window
   Fblowup = 1;                                                            //  magnify small image to window size
   Ffuncbusy++;

   for (imx = 0; imx < cimNF; imx++) {
      cimOffs[imx].xf = cimOffs[imx].xf / cimScale;                        //  scale x/y offsets for full-size images
      cimOffs[imx].yf = cimOffs[imx].yf / cimScale;
   }

   cimScale = 1.0;                                                         //  full-size

   for (imx = 0; imx < cimNF; imx++) {
      PXM_free(cimPXMs[imx]);
      cimPXMs[imx] = PXM_copy(cimPXMf[imx]);                               //  copy full-size images
      cim_curve_Vimage(imx);                                               //  curve them
   }

   cimBlend = 0.3 * cimPXMs[0]->hh;
   cim_get_overlap(0,1,cimPXMs);                                           //  match images 0 & 1 in overlap area
   cim_match_colors(0,1,cimPXMs);
   cim_adjust_colors(cimPXMf[0],1);                                        //  image 0 << profile 1
   cim_adjust_colors(cimPXMf[1],2);                                        //  image 1 << profile 2

   if (cimNF > 2) {
      cimBlend = 0.3 * cimPXMs[1]->hh;
      cim_get_overlap(1,2,cimPXMs);
      cim_match_colors(1,2,cimPXMs);
      cim_adjust_colors(cimPXMf[0],1);
      cim_adjust_colors(cimPXMf[1],1);
      cim_adjust_colors(cimPXMf[2],2);
   }

   if (cimNF > 3) {
      cimBlend = 0.3 * cimPXMs[2]->hh;
      cim_get_overlap(2,3,cimPXMs);
      cim_match_colors(2,3,cimPXMs);
      cim_adjust_colors(cimPXMf[0],1);
      cim_adjust_colors(cimPXMf[1],1);
      cim_adjust_colors(cimPXMf[2],1);
      cim_adjust_colors(cimPXMf[3],2);
   }

   cimScale = panInitAlignSize / cimPXMf[1]->hh;                           //  initial align image scale
   if (cimScale > 1.0) cimScale = 1.0;

   for (imx = 0; imx < cimNF; imx++) {                                     //  scale offsets for image scale
      cimOffs[imx].xf = cimOffs[imx].xf * cimScale;
      cimOffs[imx].yf = cimOffs[imx].yf * cimScale;
   }
   
   cimSearchRange = panInitSearchRange;                                    //  initial align search range
   cimSearchStep = panInitSearchStep;                                      //  initial align search step
   cimWarpRange = panInitWarpRange;                                        //  initial align corner warp range
   cimWarpStep = panInitWarpStep;                                          //  initial align corner warp step
   hh = cimPXMf[0]->hh * cimScale;                                         //  initial align image width
   cimBlend = hh * panInitBlend;                                           //  initial align blend width
   cimSampSize = panSampSize;                                              //  pixel sample size for align/compare
   cimNsearch = 0;                                                         //  reset align search counter
   
   while (true)                                                            //  loop, increasing image size
   {
      for (imx = 0; imx < cimNF; imx++) {                                  //  prepare images
         cim_scale_image(imx,cimPXMs);                                     //  scale to new size
         cim_curve_Vimage(imx);                                            //  curve based on lens params
         cim_warp_image_Vpano(imx,1);                                      //  apply corner warps
      }
      
      cim_show_Vimages(1,0);                                               //  show with 50/50 blend in overlaps

      for (im1 = 0; im1 < cimNF-1; im1++)                                  //  fine-align each image with top neighbor
      {
         im2 = im1 + 1;

         offsets0 = cimOffs[im2];                                          //  save initial alignment offsets
         overlap = cim_get_overlap(im1,im2,cimPXMs);                       //  get overlap area                v.11.04
         if (overlap < panFinalBlend-2) {
            zmessageACK(mWin,ZTX("Too little overlap, cannot align"));
            goto fail;
         }

         cim_get_redpix(im1);                                              //  get high-contrast pixels
         
         cim_align_image(im1,im2);                                         //  search for best offsets and warps
         
         zfree(cimRedpix);                                                 //  clear red pixels
         cimRedpix = 0;

         dx = cimOffs[im2].xf - offsets0.xf;                               //  changes from initial offsets
         dy = cimOffs[im2].yf - offsets0.yf;
         dt = cimOffs[im2].tf - offsets0.tf;
         
         for (imx = im2+1; imx < cimNF; imx++)                             //  propagate to following images
         {
            cimOffs[imx].xf += dx;
            cimOffs[imx].yf += dy;
            cimOffs[imx].tf += dt;
            ww = cimOffs[imx].xf - cimOffs[im2].xf;
            cimOffs[imx].yf += ww * dt;
         }
      }
      
      if (cimScale == 1.0) goto success;                                   //  done

      R = panImageIncrease;                                                //  next larger image size
      cimScale = cimScale * R;
      if (cimScale > 0.85) {                                               //  if close to end, jump to end
         R = R / cimScale;
         cimScale = 1.0;
      }

      for (imx = 0; imx < cimNF; imx++)                                    //  scale offsets for new size
      {
         cimOffs[imx].xf *= R;
         cimOffs[imx].yf *= R;

         for (int ii = 0; ii < 4; ii++) {
            cimOffs[imx].wx[ii] *= R;
            cimOffs[imx].wy[ii] *= R;
         }
      }

      cimSearchRange = panSearchRange;                                     //  align search range
      cimSearchStep = panSearchStep;                                       //  align search step size
      cimWarpRange = panWarpRange;                                         //  align corner warp range
      cimWarpStep = panWarpStep;                                           //  align corner warp step size

      cimBlend = cimBlend * panBlendDecrease * R;                          //  blend width, reduced
      hh = cimPXMf[0]->hh * cimScale;
      if (cimBlend < panFinalBlend * hh) 
         cimBlend = panFinalBlend * hh;                                    //  stay above minimum
   }

success: 
   panStat = 1;
   goto align_done;
fail:
   panStat = 0;
align_done:
   Fzoom = Fblowup = 0;
   Ffuncbusy--;
   cimBlend = 1;                                                           //  tiny blend (increase in tweak if wanted)
   cim_show_Vimages(0,0);
   return;
}


//  get user inputs for RGB changes and blend width, update cimPXMw[*]

void vpano_tweak()
{
   int    vpano_tweak_event(zdialog *zd, cchar *event);                    //  dialog event function
   
   cchar    *tweaktitle = ZTX("Match Brightness and Color");
   char     imageN[8] = "imageN";
   int      imx;
   
   cimBlend = 1;                                                           //  init. blend width
   
   panozd = zdialog_new(tweaktitle,mWin,Bdone,Bcancel,null);

   zdialog_add_widget(panozd,"hbox","hbim","dialog",0,"space=5");
   zdialog_add_widget(panozd,"label","labim","hbim",ZTX("image"),"space=5");      //  image  (o)  (o)  (o)  (o)
   zdialog_add_widget(panozd,"hbox","hbc1","dialog",0,"homog");                   //
   zdialog_add_widget(panozd,"label","labred","hbc1",Bred);                       //     red     green    blue
   zdialog_add_widget(panozd,"label","labgreen","hbc1",Bgreen);                   //   [_____]  [_____]  [_____]
   zdialog_add_widget(panozd,"label","labblue","hbc1",Bblue);                     //
   zdialog_add_widget(panozd,"hbox","hbc2","dialog",0,"homog");                   //   brightness [___]  [apply]
   zdialog_add_widget(panozd,"spin","red","hbc2","50|200|0.1|100","space=5");     //
   zdialog_add_widget(panozd,"spin","green","hbc2","50|200|0.1|100","space=5");   //   --------------------------
   zdialog_add_widget(panozd,"spin","blue","hbc2","50|200|0.1|100","space=5");    //
   zdialog_add_widget(panozd,"hbox","hbbri","dialog",0,"space=5");                //   [auto color]  [file color]
   zdialog_add_widget(panozd,"label","labbr","hbbri",Bbrightness,"space=5");      //
   zdialog_add_widget(panozd,"spin","bright","hbbri","50|200|0.1|100");           //   --------------------------
   zdialog_add_widget(panozd,"button","brapp","hbbri",Bapply,"space=10");         //
   zdialog_add_widget(panozd,"hsep","hsep","dialog",0,"space=5");                 //   blend width [___] [apply]   
   zdialog_add_widget(panozd,"hbox","hbc3","dialog",0,"space=5");                 //
   zdialog_add_widget(panozd,"button","auto","hbc3",ZTX("auto color"),"space=5"); //          [done]  [cancel]
   zdialog_add_widget(panozd,"button","file","hbc3",ZTX("file color"),"space=5");
   zdialog_add_widget(panozd,"hsep","hsep","dialog",0,"space=5");
   zdialog_add_widget(panozd,"hbox","hbblen","dialog",0);
   zdialog_add_widget(panozd,"label","labbl","hbblen",Bblendwidth,"space=5");
   zdialog_add_widget(panozd,"spin","blend","hbblen","1|300|1|1");
   zdialog_add_widget(panozd,"button","blapp","hbblen",Bapply,"space=15");
   
   for (imx = 0; imx < cimNF; imx++) {                                     //  add radio button per image
      imageN[5] = '0' + imx;
      zdialog_add_widget(panozd,"radio",imageN,"hbim",0,"space=5");
   }
   
   zdialog_stuff(panozd,"image0",1);                                       //  pre-select 1st image
   zdialog_resize(panozd,300,0);

   panStat = -1;                                                           //  busy status
   zdialog_run(panozd,vpano_tweak_event,"-10/20");                         //  run dialog, parallel            v.11.07
   zdialog_wait(panozd);                                                   //  wait for dialog completion
   return;
}


//  dialog event function

int vpano_tweak_event(zdialog *zd, cchar *event)
{
   char        imageN[8] = "imageN";
   double      red, green, blue, bright, bright2;
   double      red1, green1, blue1;
   int         nn, im0, imx, im1, im2, ww, hh, px, py;
   uint16      *pixel;
   
   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) panStat = 1;                                     //  done
      if (zd->zstat == 2) panStat = 0;                                     //  cancel
      zdialog_free(panozd);                                                //  kill dialog
      return 0;
   }

   for (im0 = 0; im0 < cimNF; im0++) {                                     //  get which image is selected
      imageN[5] = '0' + im0;                                               //    by the radio buttons
      zdialog_fetch(zd,imageN,nn);
      if (nn) break;
   }
   if (im0 == cimNF) return 1;

   zdialog_fetch(zd,"red",red);                                            //  get color adjustments
   zdialog_fetch(zd,"green",green);
   zdialog_fetch(zd,"blue",blue);
   zdialog_fetch(zd,"bright",bright);                                      //  brightness adjustment

   bright2 = (red + green + blue) / 3;                                     //  RGB brightness
   bright = bright / bright2;                                              //  bright setpoint / RGB brightness
   red = red * bright;                                                     //  adjust RGB brightness
   green = green * bright;
   blue = blue * bright;
   
   bright = (red + green + blue) / 3;
   zdialog_stuff(zd,"red",red);                                            //  force back into consistency
   zdialog_stuff(zd,"green",green);
   zdialog_stuff(zd,"blue",blue);
   zdialog_stuff(zd,"bright",bright);
   
   if (strEqu(event,"brapp"))                                              //  apply color & brightness changes
   {
      red = red / 100;                                                     //  normalize 0.5 ... 2.0
      green = green / 100;
      blue = blue / 100;
      
      cim_warp_image_Vpano(im0,0);                                         //  refresh cimPXMw from cimPXMs

      ww = cimPXMw[im0]->ww;
      hh = cimPXMw[im0]->hh;
      
      for (py = 0; py < hh; py++)                                          //  loop all image pixels
      for (px = 0; px < ww; px++)
      {
         pixel = PXMpix(cimPXMw[im0],px,py);
         red1 = red * pixel[0];                                            //  apply color factors
         green1 = green * pixel[1];
         blue1 = blue * pixel[2];
         if (! blue1) continue;

         if (red1 > 65535 || green1 > 65535 || blue1 > 65535) {
            bright = red1;                                                 //  avoid overflow
            if (green1 > bright) bright = green1;
            if (blue1 > bright) bright = blue1;
            bright = 65535.0 / bright;
            red1 = red1 * bright;
            green1 = green1 * bright;
            blue1 = blue1 * bright;
         }
         
         if (blue1 < 1) blue1 = 1;                                         //  avoid 0

         pixel[0] = red1;
         pixel[1] = green1;
         pixel[2] = blue1;
      }
      
      cimBlend = 1;
      zdialog_stuff(zd,"blend",cimBlend);
      cim_show_Vimages(0,0);                                               //  combine and show with 50/50 blend
   }

   if (strEqu(event,"auto"))                                               //  auto match color of selected image
   {
      for (im1 = im0; im1 < cimNF-1; im1++)                                //  from selected image to last image
      {
         im2 = im1 + 1;
         cimBlend = 0.3 * cimPXMw[im2]->hh;
         cim_get_overlap(im1,im2,cimPXMw);                                 //  match images in overlap area
         cim_match_colors(im1,im2,cimPXMw);
         cim_adjust_colors(cimPXMw[im1],1);                                //  image im1 << profile 1
         cim_adjust_colors(cimPXMw[im2],2);                                //  image im2 << profile 2
         for (imx = im1-1; imx >= im0; imx--)
            cim_adjust_colors(cimPXMw[imx],1);
         cimBlend = 1;
         zdialog_stuff(zd,"blend",cimBlend);
         cim_show_Vimages(0,0);
      }

      for (im1 = im0-1; im1 >= 0; im1--)                                   //  from selected image to 1st image
      {
         im2 = im1 + 1;
         cimBlend = 0.3 * cimPXMw[im2]->hh;
         cim_get_overlap(im1,im2,cimPXMw);                                 //  match images in overlap area
         cim_match_colors(im1,im2,cimPXMw);
         cim_adjust_colors(cimPXMw[im1],1);                                //  image im1 << profile 1
         cim_adjust_colors(cimPXMw[im2],2);                                //  image im2 << profile 2
         for (imx = im2+1; imx < cimNF; imx++)
            cim_adjust_colors(cimPXMw[imx],2);
         cimBlend = 1;
         zdialog_stuff(zd,"blend",cimBlend);
         cim_show_Vimages(0,0);
      }
   }
   
   if (strEqu(event,"file"))                                               //  use original file colors
   {
      if (! cim_load_files()) return 1;

      for (imx = 0; imx < cimNF; imx++) {
         PXM_free(cimPXMs[imx]);
         cimPXMs[imx] = PXM_copy(cimPXMf[imx]);
         cim_curve_Vimage(imx);                                            //  curve and warp
         cim_warp_image_Vpano(imx,0);
      }

      cimBlend = 1;
      zdialog_stuff(zd,"blend",cimBlend);
      cim_show_Vimages(0,0);
   }
   
   if (strEqu(event,"blapp"))                                              //  apply new blend width
   {
      zdialog_fetch(zd,"blend",cimBlend);                                  //  can be zero
      cim_show_Vimages(0,1);                                               //  show with gradual blend
   }

   return 1;
}


