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

   Fotoxx      edit photos and manage collections

   Copyright 2007-2016 Michael Cornelison
   Source URL: http://kornelix.net
   Contact: kornelix@posteo.de

   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/.

*********************************************************************************

   Fotoxx image edit functions - Combine menu functions

   cim_load_files          load image all files into pixmaps
   cim_scale_image         scale image to cimScale <= 1.0
   cim_get_overlap         get overlap area for two curved images
   cim_match_colors        use overlap area to calibrate color differences
   cim_adjust_colors       modify images to make the colors match
   cim_get_redpix          find high-contrast pixels in overlap area
   cim_curve_image         curve image for panorama stitching
   cim_warp_image          warp image corners to improve alignment
   cim_align_image         optimize alignment offsets and corner warps
   cim_match_images        compute level of image alignment
   cim_show_images         show all images aligned and combined
   cim_trim                cut off unmatched edges of aligned images
   m_HDR                   combine and adjust images with different exposures
   m_HDF                   combine and adjust images with different focus depth
   m_STP                   combine multiple photos to eliminate transient objects
   m_STN                   combine multiple photos to reduce noise
   m_pano                  combine overlapped images horizontally
   m_vpano                 combine overlapped images vertically
   m_pano_PT               combine overlapped images using Panorama Tools

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

#define EX extern                                                                //  enable extern declarations
#include "fotoxx.h"                                                              //  (variables in fotoxx.h are refs)

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


//   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
   float    xf, yf, tf;                                           //  x, y, theta offsets
   float    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

int      pixcc = 4 * sizeof(float);                               //  RGB pixel cc with alpha channel             15.09

float    cimScale;                                                //  alignment image size relative to full image
float    cimBlend;                                                //  image blend width at overlap, pixels (pano)
float    cimSearchRange;                                          //  alignment search range, pixels
float    cimSearchStep;                                           //  alignment search step, vpixels
int      cim_manualwarp;                                          //  alignmant manual warping instead of auto
int      cim_manualalign;                                         //  alignment is manual instead of auto         16.01
float    cimWarpRange;                                            //  alignment corner warp range, pixels
float    cimWarpStep;                                             //  alignment corner warp step, vpixels
float    cimSampSize;                                             //  pixel sample size
int      cimOv1xlo, cimOv1xhi, cimOv1ylo, cimOv1yhi;              //  rectangle enclosing overlap area,
int      cimOv2xlo, cimOv2xhi, cimOv2ylo, cimOv2yhi;              //    image 1 and image2 coordinates
float    cimRGBmf1[3][256];                                       //  RGB matching factors for image 1/2 compare:
float    cimRGBmf2[3][256];                                       //  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      cimPano;                                                 //  pano mode flag for cim_align_image()
int      cimPanoV;                                                //  vertical pano flag
int      cimPanoNC;                                               //  pano no-curve flag (scanned image)
float    cimPanoFL;                                               //  pano lens focal length at image scale

int      cim_load_files();                                        //  load and check selected files
void     cim_scale_image(int im, PXM **);                         //  scale image, 1.0 to cimScale (normally < 1)
float    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_adjust_colors_pano(PXM *pxm, int fwhich);            //  adjust RGB levels from match data
void     cim_adjust_colors_vpano(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
float    cim_match_images(int im1, int im2);                      //  compute match for overlapped images
void     cim_show_images(int fnew, int fblend);                   //  combine images >> E3pxm >> 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 *label);                          //  diagnostic tool


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

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

int cim_load_files()
{
   PXM      *pxm;

   for (int imx = 0; imx < cimNF; imx++)
   {
      PXM_free(cimPXMf[imx]);
      pxm = PXM_load(cimFile[imx],1);                                            //  ACK errors
      if (! pxm) return 0;
      PXM_addalpha(pxm);                                                         //  15.09
      cimPXMf[imx] = pxm;
   }

   return 1;
}


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

void cim_scale_image(int im, PXM** pxmout)
{
   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);

   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

float  cim_get_overlap(int im1, int im2, PXM **pxmx)
{
   float       x1, y1, t1, x2, y2, t2;
   float       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) * cosf(t1) + (y2 - y1) * sinf(t1);                           //  offset of im2 relative to im1
   yoff = (y2 - y1) * cosf(t1) - (x2 - x1) * sinf(t1);
   toff = t2 - t1;

   costf = cosf(toff);
   sintf = sinf(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);

   if (cimPanoV) {
      if (cimBlend && cimBlend < (cimOv1yhi - cimOv1ylo)) {                      //  reduce y range to cimBlend
         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
   else return 1.0 * (cimOv1xhi - cimOv1xlo) / ww1;                              //  return overlap width <= 1.0
}


//  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)
{
   float       Bratios1[3][256];                //  image2/image1 brightness ratio per color per level
   float       Bratios2[3][256];                //  image1/image2 brightness ratio per color per level

   float       *pix1, vpix2[4];
   int         vstat2, px1, py1;
   int         ii, jj, rgb;
   int         npix, npix1, npix2, npix3;
   int         brdist1[3][256], brdist2[3][256];
   float       x1, y1, t1, x2, y2, t2;
   float       xoff, yoff, toff, costf, sintf;
   float       px2, py2;
   float       brlev1[3][256], brlev2[3][256];
   float       a1, a2, b1, b2, bratio = 1;
   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) * cosf(t1) + (y2 - y1) * sinf(t1);                           //  offset of im2 relative to im1
   yoff = (y2 - y1) * cosf(t1) - (x2 - x1) * sinf(t1);
   toff = t2 - t1;

   costf = cosf(toff);
   sintf = sinf(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[3] < 254) continue;                                               //  ignore void pixels                 15.09

      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
      if (vpix2[3] < 254) continue;                                              //  ignore void pixels                 15.09

      ++npix;                                                                    //  count overlapping pixels

      for (rgb = 0; rgb < 3; rgb++)                                              //  accumulate distributions
      {                                                                          //    by color in 256 bins
         ++brdist1[rgb][int(pix1[rgb])];
         ++brdist2[rgb][int(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 < 256; ii++)                                                  //  convert brightness ratios
   for (rgb = 0; rgb < 3; rgb++)                                                 //    to conversion factors
   {
      cimRGBmf1[rgb][ii] = sqrt(Bratios1[rgb][ii]) * ii;                         //  use sqrt(ratio) so that adjustment
      cimRGBmf2[rgb][ii] = sqrt(Bratios2[rgb][ii]) * 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)
{
   int         ww, hh, px, py;
   float       red, green, blue, max;
   float       *pix;
   float       ff;

   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][int(red)];
         green = cimRGBmf1[1][int(green)];
         blue = cimRGBmf1[2][int(blue)];
      }

      else {
         red = cimRGBmf2[0][int(red)];
         green = cimRGBmf2[1][int(green)];
         blue = cimRGBmf2[2][int(blue)];
      }

      if (red > 255.9 || green > 255.9 || blue > 255.9) {
         max = red;
         if (green > max) max = green;
         if (blue > max) max = blue;
         ff = 255.9 / max;
         red = red * ff;
         green = green * ff;
         blue = blue * ff;
      }

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

   return;
}


//  Use color match data from cim_match_colors() to
//    modify images so the colors match.
//  fwhich = 1/2 = left/right image

void cim_adjust_colors_pano(PXM *pxm, int fwhich)
{
   int         ww, hh, ww2, px, py;
   int         pxlo, pxhi;
   float       red1, green1, blue1, max;
   float       red2, green2, blue2;
   float       *pix;
   float       f1, f2;

   ww = pxm->ww;
   hh = pxm->hh;
   ww2 = ww / 2;

   if (fwhich == 1) {                                                            //  scan half the image
      pxlo = ww2;
      pxhi = ww;
   }
   else {
      pxlo = 0;
      pxhi = ww2;
   }

   for (py = 0; py < hh; py++)
   for (px = pxlo; px < pxhi; px++)
   {
      pix = PXMpix(pxm,px,py);
      red1 = pix[0];
      green1 = pix[1];
      blue1 = pix[2];
      if (! blue1) continue;

      if (fwhich == 1) {
         red2 = cimRGBmf1[0][int(red1)];
         green2 = cimRGBmf1[1][int(green1)];
         blue2 = cimRGBmf1[2][int(blue1)];
      }

      else {
         red2 = cimRGBmf2[0][int(red1)];
         green2 = cimRGBmf2[1][int(green1)];
         blue2 = cimRGBmf2[2][int(blue1)];
      }

      if (fwhich == 1) {                                                         //  taper color adjustment over distance
         f2 = 1.0 * (px - pxlo) / ww2;
         f1 = 1.0 - f2;
      }
      else {
         f1 = 1.0 * px / ww2;
         f2 = 1.0 - f1;
      }

      red2 = f2 * red2 + f1 * red1;
      green2 = f2 * green2 + f1 * green1;
      blue2 = f2 *  blue2 + f1 * blue1;

      if (red2 > 255.9 || green2 > 255.9 || blue2 > 255.9) {
         max = red2;
         if (green2 > max) max = green2;
         if (blue2 > max) max = blue2;
         f1 = 255.9 / max;
         red2 = red2 * f1;
         green2 = green2 * f1;
         blue2 = blue2 * f1;
      }

      pix[0] = red2;
      pix[1] = green2;
      pix[2] = blue2;
   }

   return;
}


//  version for vertical panoramas
//  fwhich = 1/2 = upper/lower image

void cim_adjust_colors_vpano(PXM *pxm, int fwhich)
{
   int         ww, hh, hh2, px, py;
   int         pylo, pyhi;
   float       red1, green1, blue1, max;
   float       red2, green2, blue2;
   float       *pix;
   float       f1, f2;

   ww = pxm->ww;
   hh = pxm->hh;
   hh2 = hh / 2;

   if (fwhich == 1) {                                                            //  scan half the image
      pylo = hh2;
      pyhi = hh;
   }
   else {
      pylo = 0;
      pyhi = hh2;
   }

   for (py = pylo; py < pyhi; py++)
   for (px = 0; px < ww; px++)
   {
      pix = PXMpix(pxm,px,py);
      red1 = pix[0];
      green1 = pix[1];
      blue1 = pix[2];
      if (! blue1) continue;

      if (fwhich == 1) {
         red2 = cimRGBmf1[0][int(red1)];
         green2 = cimRGBmf1[1][int(green1)];
         blue2 = cimRGBmf1[2][int(blue1)];
      }

      else {
         red2 = cimRGBmf2[0][int(red1)];
         green2 = cimRGBmf2[1][int(green1)];
         blue2 = cimRGBmf2[2][int(blue1)];
      }

      if (fwhich == 1) {                                                         //  taper color adjustment over distance
         f2 = 1.0 * (py - pylo) / hh2;
         f1 = 1.0 - f2;
      }
      else {
         f1 = 1.0 * py / hh2;
         f2 = 1.0 - f1;
      }

      red2 = f2 * red2 + f1 * red1;
      green2 = f2 * green2 + f1 * green1;
      blue2 = f2 *  blue2 + f1 * blue1;

      if (red2 > 255.9 || green2 > 255.9 || blue2 > 255.9) {
         max = red2;
         if (green2 > max) max = green2;
         if (blue2 > max) max = blue2;
         f1 = 255.9 / max;
         red2 = red2 * f1;
         green2 = green2 * f1;
         blue2 = blue2 * f1;
      }

      pix[0] = red2;
      pix[1] = green2;
      pix[2] = blue2;
   }

   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)
{
   int         ww, hh, samp, xzone, yzone;
   int         pxL, pxH, pyL, pyH;
   int         px, py, ii, jj, npix;
   int         ov1xlo, ov1xhi, ov1ylo, ov1yhi;
   int         Hdist[256], Vdist[256], Hmin, Vmin;
   float       zsamp[16] = { 5,5,5,5, 7,8,8,7, 7,8,8,7, 5,5,5,5 };               //  % sample per zone, sum = 100
   uchar       *Hcon, *Vcon;
   float       *pix1, *pix2;
   PXM         *pxm;
   
   pxm = cimPXMs[im1];
   ww = pxm->ww;
   hh = pxm->hh;

   if (cimRedpix) zfree(cimRedpix);                                              //  clear prior
   cimRedpix = (char *) zmalloc(ww*hh);
   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
   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);                                            //  horizontal pixel contrast 0-255
      Vcon = (uchar *) zmalloc(npix);                                            //  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[3] < 254) continue;                                            //  avoid void pixels                  15.09
         pix1 = PXMpix(pxm,px+6,py);
         if (pix1[3] < 254) continue;
         pix1 = PXMpix(pxm,px,py+6);
         if (pix1[3] < 254) continue;
         pix1 = PXMpix(pxm,px-6,py);
         if (pix1[3] < 254) continue;
         
         pix1 = PXMpix(pxm,px,py);                                               //  candidate red pixel
         pix2 = PXMpix(pxm,px+2,py);                                             //  2 pixels to right
         Hcon[ii] = 255 * (1.0 - PIXMATCH(pix1,pix2));                           //  horz. contrast 0-255

         pix2 = PXMpix(pxm,px,py+2);                                             //  2 pixels below
         Vcon[ii] = 255 * (1.0 - PIXMATCH(pix1,pix2));                           //  vert. contrast
      }

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

      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--)                                     //  (vertical 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] + Vcon[ii] < 20) continue;                                 //  ignore low contrast pixels
         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];
         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 mm (pano)
//  replaces cimPXMs[im] with curved version

void cim_curve_image(int im)                                                     //  overhauled                         15.01
{
   int         px, py, ww, hh, vstat;
   float       ww2, hh2;
   float       dx, dy;
   float       sx, sy;
   float       tx, ty;
   float       F = lens_mm;                                                      //  lens focal length, 35mm equivalent
   float       S = 35.0;                                                         //  corresponding image width
   PXM         *pxmin, *pxmout;
   float       vpix[4], *pix;

   if (cimPanoNC) return;                                                        //  no-curve flag

   pxmin = cimPXMs[im];                                                          //  input and output image
   ww = pxmin->ww;
   hh = pxmin->hh;
   ww2 = ww / 2;
   hh2 = hh / 2;

   if (ww > hh) F = F / S * ww;
   else F = F / S * hh;

   pxmout = PXM_make(ww,hh,4);                                                   //  temp. output PXM

   for (py = 0; py < hh; py++)                                                   //  cylindrical projection
   for (px = 0; px < ww; px++)
   {
      sx = px - ww2;
      sy = py - hh2;
      tx = sx / F;
      ty = sy / F;
      dx = F * tanf(tx);
      dy = F * tanf(ty) / cosf(tx);
      dx += ww2;
      dy += hh2;
      vstat = vpixel(pxmin,dx,dy,vpix);                                          //  input virtual pixel
      pix = PXMpix(pxmout,px,py);                                                //  output real pixel
      if (vstat) memcpy(pix,vpix,pixcc);
      else memset(pix,0,pixcc);                                                  //  voided pixels are (0,0,0,0)
   }

   PXM_free(pxmin);                                                              //  replace input with output PXM
   cimPXMs[im] = pxmout;

   cimPanoFL = F;                                                                //  save focal length globally
   return;
}


//  unbend the combined panorama image

void cim_uncurve_image()                                                         //  15.01
{
   PXM      *pxmout;
   int      ww, hh, vstat;
   float    ww2, hh2;
   int      px, py;
   int      rx, ry;
   float    dx, dy;
   float    sx, sy;
   float    tx, ty;
   float    F = cimPanoFL;
   float    *pix, vpix[4];
   float    *pix1, *pix2;

   ww = E3pxm->ww;
   hh = E3pxm->hh;
   
   pxmout = PXM_make(1.4*ww,1.2*hh,4);
   if (! pxmout) return;                                                         //  too big, do nothing                16.04

   for (ry = 0; ry < hh; ry++)
   for (rx = 0; rx < ww; rx++)
   {
      px = rx + 0.2 * ww;
      py = ry + 0.1 * hh;                                                        //  expand margins                     16.04 
      pix1 = PXMpix(E3pxm,rx,ry);
      pix2 = PXMpix(pxmout,px,py);
      memcpy(pix2,pix1,pixcc);
   }
   
   PXM_free(E3pxm);
   E3pxm = pxmout;

   ww = E3pxm->ww;
   hh = E3pxm->hh;
   
   pxmout = PXM_make(ww,hh,4);
   if (! pxmout) return;                                                         //  16.04

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

   for (py = 0; py < hh; py++)
   for (px = 0; px < ww; px++)
   {
      dx = px - ww2;
      tx = atanf(dx / F);
      sx = F * tx;
      sx += ww2;

      dy = py - hh2;
      ty = atanf(dy / F * cosf(tx));
      sy = F * ty;
      sy += hh2;

      vstat = vpixel(E3pxm,sx,sy,vpix);                                          //  input virtual pixel
      pix = PXMpix(pxmout,px,py);                                                //  output real pixel
      if (vstat) memcpy(pix,vpix,pixcc);
      else memset(pix,0,pixcc);                                                  //  voided pixel
   }

   PXM_free(E3pxm);                                                              //  replace input with output PXM
   E3pxm = pxmout;
}


//  version for vertical panorama

void cim_curve_Vimage(int im)
{
   int         px, py, ww, hh, vstat;
   float       ww2, hh2;
   float       dx, dy;
   float       sx, sy;
   float       tx, ty;
   float       F = lens_mm;
   float       S = 35.0;
   PXM         *pxmin, *pxmout;
   float       vpix[4], *pix;

   if (cimPanoNC) return;

   pxmin = cimPXMs[im];
   ww = pxmin->ww;
   hh = pxmin->hh;
   ww2 = ww / 2;
   hh2 = hh / 2;

   if (ww > hh) F = F / S * ww;
   else F = F / S * hh;

   pxmout = PXM_make(ww,hh,4);

   for (py = 0; py < hh; py++)
   for (px = 0; px < ww; px++)
   {
      sx = px - ww2;
      sy = py - hh2;
      tx = sx / F;
      ty = sy / F;
      dy = F * tanf(ty);                                                         //  these two lines
      dx = F * tanf(tx) / cosf(ty);                                              //  are different
      dx += ww2;
      dy += hh2;
      vstat = vpixel(pxmin,dx,dy,vpix);
      pix = PXMpix(pxmout,px,py);
      if (vstat) memcpy(pix,vpix,pixcc);
      else memset(pix,0,pixcc);                                                  //  voided pixel
   }

   PXM_free(pxmin);
   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;
   float       ww, hh, wwi, hhi;
   float       wx0, wy0, wx1, wy1, wx2, wy2, wx3, wy3;
}

void cim_warp_image(int im)                                                      //  caller function
{
   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);
   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,&Nval[ii]);
   wait_wthreads();                                                              //  wait for completion

   return;
}


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

   int         index = *((int *) arg);
   int         pxm, pym, vstat;
   float       vpix[4], *pixm;
   float       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) memcpy(pixm,vpix,pixcc);
      else memset(pixm,0,pixcc);                                                 //  voided pixel
   }

   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)
{
   int         ww, hh, ww2, hh2, pxL, pxH;
   int         pxm, pym, vstat;
   float       vpix[4], *pixm;
   float       ww2i, hh2i, pxs, pys, xdisp, ydisp;
   float       wx0, wy0, wx3, wy3;
   PXM         *pxmin, *pxmout;

   pxmin = cimPXMs[im];                                                          //  input and output pixmaps
   pxmout = cimPXMw[im];

   PXM_free(pxmout);
   pxmout = PXM_copy(pxmin);
   cimPXMw[im] = pxmout;

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

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

   ww2i = 1.0 / ww2;
   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
   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;

      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) memcpy(pixm,vpix,pixcc);
      else memset(pixm,0,pixcc);                                                 //  voided pixel
   }

   return;
}


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

void cim_warp_image_Vpano(int im, int fblend)
{
   int         ww, hh, ww2, hh2, pyL, pyH;
   int         pxm, pym, vstat;
   float       vpix[4], *pixm;
   float       ww2i, hh2i, pxs, pys, xdisp, ydisp;
   float       wx0, wy0, wx1, wy1;
   PXM         *pxmin, *pxmout;

   pxmin = cimPXMs[im];                                                          //  input and output pixmaps
   pxmout = cimPXMw[im];

   PXM_free(pxmout);
   pxmout = PXM_copy(pxmin);
   cimPXMw[im] = pxmout;

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

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

   ww2i = 1.0 / ww2;
   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
   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) memcpy(pixm,vpix,pixcc);
      else memset(pixm,0,pixcc);                                                 //  voided pixel
   }

   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)
{
   int         ii, corner1, cornerstep, cornerN, pass;
   float       xyrange, xystep, trange, tstep, wrange, wstep;
   float       xfL, xfH, yfL, yfH, tfL, tfH;
   float       wxL, wxH, wyL, wyH;
   float       match, matchB;
   cimoffs     offsets0, offsetsB;

   Ffuncbusy = 1;

   offsets0 = cimOffs[im2];                                                      //  initial offsets
   offsetsB = offsets0;                                                          //  = best offsets so far
   matchB = cim_match_images(im1,im2);                                           //  = best image match level

   for (pass = 1; pass <=2; pass++)                                              //  main pass and 2nd pass
   {
      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;
         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];
         }

         snprintf(paneltext,200,"align: %d  match: %.5f",cimNsearch++,matchB);
         zmainloop();
      }

      cimOffs[im2] = offsetsB;                                                   //  restore best match

      if (cim_manualwarp) continue;                                              //  skip auto warp

      if (cimPano) cim_warp_image_pano(im2,1);                                   //  apply prior corner warps
      else if (cimPanoV) cim_warp_image_Vpano(im2,1);
      else  cim_warp_image(im2);

      //  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
         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].wy[ii] = wyL; cimOffs[im2].wy[ii] < wyH; cimOffs[im2].wy[ii] += wstep)
         for (cimOffs[im2].wx[ii] = wxL; cimOffs[im2].wx[ii] < wxH; cimOffs[im2].wx[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];
            }

            snprintf(paneltext,200,"warp: %d  match: %.5f",cimNsearch++,matchB);
            zmainloop();
         }

         cimOffs[im2] = offsetsB;                                                //  restore best match

         if (cimPano) cim_warp_image_pano(im2,1);                                //  apply corner warps
         else if (cimPanoV) cim_warp_image_Vpano(im2,1);
         else  cim_warp_image(im2);
      }
   }

   Ffuncbusy = 0;
   return;
}


//  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 corner warps

float  cim_match_images(int im1, int im2)
{
   float       *pix1, vpix2[4];
   int         ww, hh, ww2, hh2;
   int         px1, py1, ii, vstat;
   float       wwi, hhi, ww2i, hh2i, xdisp, ydisp;
   float       wx0, wy0, wx1, wy1, wx2, wy2, wx3, wy3;
   float       dx, dy, px2, py2;
   float       x1, y1, t1, x2, y2, t2;
   float       xoff, yoff, toff, costf, sintf, coeff;
   float       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) * cosf(t1) + (y2 - y1) * sinf(t1);                           //  offset of im2 relative to im1
   yoff = (y2 - y1) * cosf(t1) - (x2 - x1) * sinf(t1);
   toff = t2 - t1;

   costf = cosf(toff);
   sintf = sinf(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[3] < 254) continue;                                            //  ignore void pixels                 15.09

         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 (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);                                     //  get virtual pixel
         if (! vstat) continue;

         match = PIXMATCH(pix1,vpix2);                                           //  compare, brightness adjusted
         cmatch += match * match;                                                //  accumulate total match             15.01
         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[3] < 254) continue;                                            //  ignore void pixels                 15.09

         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 = PIXMATCH(pix1,vpix2);                                           //  compare brightness adjusted
         cmatch += match * match;                                                //  accumulate total match             15.01
         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[3] < 254) continue;                                            //  ignore void pixels                 15.09
      
         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 = PIXMATCH(pix1,vpix2);                                           //  compare brightness adjusted
         cmatch += match * match;                                                //  accumulate total match             15.01
         maxcmatch += 1.0;
      }
   }

   return cmatch / maxcmatch;
}


//  combine and show all images
//  used for all composite image functions except Vpano.
//  fnew >> make new E3 output image and adjust x and y offsets
//  cimPXMw[*] >> E3pxm >> main window
//  fblend:  0 > 50/50 blend,  1 > gradual blend
//  CALLED FROM THREADS as well as from main()

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

void cim_show_images(int fnew, int fblend)
{
   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;
   float       xf, yf, tf;
   float       *pix3;

   paintlock(1);                                                                 //  block window updates               16.02

   fblendd = fblend;                                                             //  blend 50/50 or gradual ramp

   im1 = cimShowIm1;                                                             //  two images to show
   im2 = cimShowIm2;
   iminc = im2 - im1;

   if (cimShowAll) {                                                             //  show all images
      im1 = 0;
      im2 = cimNF-1;
      iminc = 1;
   }

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

   if (fnew) PXM_free(E3pxm);                                                    //  force new output pixmap

   if (! E3pxm)                                                                  //  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
            cimOffs[imx].yf += 10;
         hhmax += 20;
      }

      E3pxm = PXM_make(wwmax,hhmax,4);                                           //  allocate output image
      E3pxm->ww = wwmax;
      E3pxm->hh = 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
      start_wthread(cim_show_images_wthread,&Nval[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;
            if (px3 < 0 || px3 > E3pxm->ww-1) continue;                          //  off top/bottom edge
            if (py3 < 0 || py3 > E3pxm->hh-1) continue;
            pix3 = PXMpix(E3pxm,px3,py3);
            pix3[0] = 255; pix3[1] = pix3[2] = 0;
         }
      }
   }

   paintlock(0);                                                                 //  unblock window updates             16.02
   Fpaint2();                                                                    //  update window
   return;
}


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

   int         index = *((int *) (arg));
   int         imx, imy;
   int         px3, py3;
   int         vstat, vstat1, vstat2;
   float       red1, green1, blue1;                                              //  float  15.09
   float       red2, green2, blue2;
   float       red3, green3, blue3, alpha3;
   float       f1, f2, px, py;
   float       vpix[4], *pix3;

   red1 = green1 = blue1 = 0;

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

   for (py3 = index; py3 < E3pxm->hh; py3 += NWT)                                //  loop E3 rows
   for (px3 = 0; px3 < E3pxm->ww; 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 (vpix[3] < 254) continue;                                            //  voided pixel                       15.09

         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;
            green3 = f1 * green1 + f2 * green2;
            blue3 = f1 * blue1 + f2 * blue2;
         }

         alpha3 = 255;
      }

      else red3 = green3 = blue3 = alpha3 = 0;                                   //  no overlapping image, pixel void

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

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


//  version for vertical panorama

void cim_show_Vimages(int fnew, int fblend)
{
   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;
   float       xf, yf, tf;
   float       *pix3;

   paintlock(1);                                                                 //  block window updates               16.02

   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] = cosf(cimOffs[imx].tf);
      sintf[imx] = sinf(cimOffs[imx].tf);
   }

   if (fnew) PXM_free(E3pxm);                                                    //  force new output pixmap

   if (! E3pxm)                                                                  //  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;

      E3pxm = PXM_make(wwmax,hhmax,4);                                           //  allocate output image
      E3pxm->ww = wwmax;
      E3pxm->hh = 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
      start_wthread(cim_show_Vimages_wthread,&Nval[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;
            if (px3 < 0 || px3 > E3pxm->ww-1) continue;                          //  off left/right edge
            pix3 = PXMpix(E3pxm,px3,py3);
            pix3[0] = 255; pix3[1] = pix3[2] = 0;
         }
      }
   }

   paintlock(0);                                                                 //  unblock window updates             16.02
   Fpaint2();                                                                    //  update window
   return;
}


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

   int         index = *((int *) (arg));
   int         imx, imy;
   int         px3, py3;
   int         vstat, vstat1, vstat2;
   float       red1, green1, blue1, alpha1;                                      //  float  15.09
   float       red2, green2, blue2, alpha2;
   float       red3, green3, blue3, alpha3;
   float       f1, f2, px, py;
   float       vpix[4], *pix3;

   red1 = green1 = blue1 = 0;
   alpha1 = 255;                                                                 //  alpha  15.09

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

   for (py3 = index; py3 < E3pxm->hh; py3 += NWT)                                //  loop E3 rows
   for (px3 = 0; px3 < E3pxm->ww; 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 (vpix[3] < 254) continue;                                            //  voided pixel                       15.09

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

      imx = imy;                                                                 //  first of 1 or 2 overlapping images

      if (vstat1) {
         if (! vstat2) {
            red3 = red1;                                                         //  use image1 pixel
            green3 = green1;
            blue3 = blue1;
            alpha3 = alpha1;
         }
         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;
            green3 = f1 * green1 + f2 * green2;
            blue3 = f1 * blue1 + f2 * blue2;
            alpha3 = f1 * alpha1 + f2 * alpha2;
         }
      }

      else red3 = green3 = blue3 = alpha3 = 0;                                   //  no overlapping image, voided pixel

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

   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()
{
   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;
   float    xf, yf, tf, sintf, costf, px, py, wx, wy;
   float    *pix3, *pix9;

   wwmin = hhmin = 0;
   wwmax = E3pxm->ww;
   hhmax = E3pxm->hh;

   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 = sinf(tf);
      costf = cosf(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 * E3pxm->ww) return;                                             //  sanity check
   if (hh < 0.7 * E3pxm->hh) return;

   E9pxm = PXM_make(ww,hh,4);

   for (py3 = hhmin; py3 < hhmax; py3++)                                         //  E9 = trimmed E3
   for (px3 = wwmin; px3 < wwmax; px3++)
   {
      px9 = px3 - wwmin;
      py9 = py3 - hhmin;
      pix3 = PXMpix(E3pxm,px3,py3);
      pix9 = PXMpix(E9pxm,px9,py9);
      memcpy(pix9,pix3,pixcc);
   }

   PXM_free(E3pxm);                                                              //  E3 = E9
   E3pxm = E9pxm;
   E9pxm = 0;
   E3pxm->ww = ww;
   E3pxm->hh = hh;

   return;
}


//  dump offsets to stdout - diagnostic tool

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

   for (int imx = 0; imx < cimNF; imx++)
   {
      printz(" 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
float    HDRinitAlignSize = 160;                                                 //  initial align image size
float    HDRimageIncrease = 1.6;                                                 //  image size increase per align cycle
float    HDRsampSize = 6000;                                                     //  pixel sample size   11.03

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

float    *HDRbright = 0;                                                         //  maps brightness per pixel
zdialog  *HDRadjustzd = 0;                                                       //  adjust dialog
float    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_adjust();                                                             //  adjust image contribution curves
void * HDR_adjust_thread(void *);                                                //  combine images per contribution curves

editfunc    EFhdr;                                                               //  edit function data


//  menu function

void m_HDR(GtkWidget *, cchar *)
{
   char        **flist, *ftemp;
   int         imx, jj, err, px, py, ww, hh;
   float       diffw, diffh;
   float       fbright[10], btemp;
   float       pixsum, fnorm = 1.0 / 256.0;
   float       *pixel;
   PXM         *pxmtemp;

   F1_help_topic = "HDR";                                                        //  help topic

   if (checkpend("all")) return;

   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;
   cim_manualwarp = 0;

   flist = gallery_getfiles();                                                   //  select images to combine
   if (! flist) goto cleanup;

   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] = zstrdup(flist[imx]);                                        //  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[cimNF-1]);                                               //  curr_file = last in list
   if (err) goto cleanup;

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

   for (imx = 0; imx < cimNF; imx++)                                             //  compute image brightness levels
   {
      ww = cimPXMf[imx]->ww;                                                     //  bugfix
      hh = cimPXMf[imx]->hh;                                                     //  image sizes can vary a little
      pixsum = 0;
      for (py = 0; py < hh; py++)
      for (px = 0; px < ww; px++)
      {
         pixel = PXMpix(cimPXMf[imx],px,py);
         pixsum += fnorm * (pixel[0] + pixel[1] + pixel[2]);
      }
      fbright[imx] = pixsum / (ww * hh);
   }

   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_adjust();                                                                 //  combine images based on user inputs
   if (HDRstat != 1) goto cancel;

   CEF->Fmods++;                                                                 //  done
   CEF->Fsaved = 0;
   edit_done(0);
   goto cleanup;

cancel:
   edit_cancel(0);

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);
   *paneltext = 0;
   return;
}


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

void * HDR_align_thread(void *)
{
   int         imx, im1, im2, ww, hh, ii, nn;
   float       R, maxtf, mintf, midtf;
   float       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 = 1;
   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 = 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);

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

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

         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
      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 * sinf(toff);
      dyoff = xoff * sinf(toff);
      cimOffs[im2].xf -= dxoff;
      cimOffs[im2].yf += dyoff;
   }

   Fzoom = Fblowup = 0;
   Ffuncbusy = 0;
   HDRstat = 1;
   thread_exit();
   return 0;
}


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

void HDR_brightness()
{
   int         px3, py3, ww, hh, imx, kk, vstat;
   float       px, py, red, green, blue;
   float       bright, maxbright, minbright;
   float       xoff, yoff, sintf[10], costf[10];
   float       norm, fnorm = 1.0 / 256.0;
   float       vpix[4], *pix3;

   Ffuncbusy = 1;

   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] = sinf(cimOffs[imx].tf);
      costf[imx] = cosf(cimOffs[imx].tf);
   }

   ww = E3pxm->ww;
   hh = E3pxm->hh;

   HDRbright = (float *) zmalloc(ww*hh*sizeof(int));                             //  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(E3pxm,px3,py3);                                           //  output pixel = black
         pix3[0] = pix3[1] = pix3[2] = pix3[3] = 0;                              //  alpha = 0                          15.09
         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(E3pxm,px3,py3);                                              //  output pixel
      pix3[0] = red * 255.9 / cimNF;
      pix3[1] = green * 255.9 / cimNF;
      pix3[2] = blue * 255.9 / 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;

   Ffuncbusy = 0;
   Fpaint2();                                                                    //  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_adjust()
{
   int    HDR_adjust_dialog_event(zdialog *zd, cchar *event);
   void   HDR_curvedit(int);

   int         imx;
   float       cww = 1.0 / (cimNF-1);

   HDRadjustzd = zdialog_new(ZTX("Adjust Image Contributions"),Mwin,Bdone,Bcancel,null);
   zdialog *zd = HDRadjustzd;
   zdialog_add_widget(zd,"frame","brframe","dialog",0,"expand|space=2");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0);
   zdialog_add_widget(zd,"label","lab11","hb1",ZTX("dark pixels"),"space=3");
   zdialog_add_widget(zd,"label","lab12","hb1",0,"expand");
   zdialog_add_widget(zd,"label","lab13","hb1",ZTX("light pixels"),"space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labf1","hb2",ZTX("file:"),"space=3");
   zdialog_add_widget(zd,"label","labf2","hb2","*");

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

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

   sd->Nspc = cimNF;                                                             //  no. curves = no. files

   for (imx = 0; imx < cimNF; imx++)                                             //  set up initial response curve
   {
      sd->fact[imx] = 1;
      sd->vert[imx] = 0;
      sd->nap[imx] = 2;
      sd->apx[imx][0] = 0.01;
      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_adjust_thread,0);                                            //  start working thread
   signal_thread();

   zdialog_resize(zd,400,360);
   zdialog_run(zd,HDR_adjust_dialog_event,"-10/20");                             //  run dialog
   zdialog_wait(zd);                                                             //  wait for completion
   zdialog_free(zd);

   return;
}


//  dialog event and completion callback function

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

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (strmatch(event,"load")) {                                                 //  load saved curve
      splcurve_load(sd);
      zdialog_stuff(zd,"labf2","*");
      signal_thread();
      return 0;
   }

   if (strmatch(event,"savecurve")) {                                            //  save curve to file
      splcurve_save(sd);
      return 0;
   }

   if (zd->zstat)                                                                //  dialog complete
   {
      zdialog_destroy(zd);                                                       //  prevent double [cancel]            15.02
      wrapup_thread(8);
      if (zd->zstat == 1) HDRstat = 1;
      else HDRstat = 0;
      if (HDRstat == 1) cim_trim();                                              //  cut-off edges
   }

   return 1;
}


//  this function is called when a curve is edited

void HDR_curvedit(int spc)
{
   cchar  *pp;

   pp = strrchr(cimFile[spc],'/');
   zdialog_stuff(HDRadjustzd,"labf2",pp+1);
   signal_thread();
   return;
}


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

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

   int         imx, ii, kk;
   float       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;
            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;
      }

      for (ii = 0; ii < NWT; ii++)                                               //  start worker threads
         start_wthread(HDR_combine_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

      Fpaint2();                                                                 //  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;
   float       sintf[10], costf[10], xoff, yoff;
   float       px, py, red, green, blue, bright, factor;
   float       vpix[4], *pix3;

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

   ww = E3pxm->ww;
   hh = E3pxm->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(E3pxm,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.

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

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

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

void * HDF_align_thread(void *);
void   HDF_adjust();
void   HDF_mousefunc();
void * HDF_adjust_thread(void *);

editfunc    EFhdf;                                                               //  edit function data


//  menu function

void m_HDF(GtkWidget *, cchar *)
{
   char        **flist;
   int         imx, err, ww, hh;
   float       diffw, diffh;

   F1_help_topic = "HDF";                                                        //  help topic

   if (checkpend("all")) return;

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

   cimNF = 0;
   cim_manualwarp = 0;

   flist = gallery_getfiles();                                                   //  select images to combine
   if (! flist) goto cleanup;

   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] = zstrdup(flist[imx]);                                        //  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[cimNF-1]);                                               //  curr_file = last in list
   if (err) goto cleanup;

   EFhdf.menufunc = m_HDF;
   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_adjust();                                                                 //  combine images based on user inputs
   if (HDFstat != 1) goto cancel;

   CEF->Fmods++;                                                                 //  done
   CEF->Fsaved = 0;
   edit_done(0);
   goto cleanup;

cancel:
   edit_cancel(0);

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]);
   }

   *paneltext = 0;
   return;
}


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

void * HDF_align_thread(void *)
{
   int         imx, im1, im2, ww, hh, ii, nn;
   float       R, maxtf, mintf, midtf;
   float       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 = 1;
   cimPano = cimPanoV = 0;                                                       //  no pano mode

   for (imx = 0; imx < cimNF; imx++)                                             //  bugfix
      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);

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

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

         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
      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 * sinf(toff);
      dyoff = xoff * sinf(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]);
      cimPXMs[imx] = PXM_copy(cimPXMw[imx]);
   }

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


//  paint and warp output image

zdialog  *HDFadjustzd = 0;                                                       //  paint/warp 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_adjust()
{
   char     imageN[8] = "imageN", labN[4] = "0";
   int      cc, imx, ww, hh;

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

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

   HDFadjustzd = zdialog_new(ZTX("Paint and Warp Image"),Mwin,Bdone,Bcancel,null);
   zdialog *zd = HDFadjustzd;

   zdialog_add_widget(zd,"hbox","hbim","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labim","hbim",Bimage,"space=5");
   zdialog_add_widget(zd,"hbox","hbpw","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbpw1","hbpw",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbpw2","hbpw",0,"homog|space=5");
   zdialog_add_widget(zd,"radio","paint","vbpw1",ZTX("paint"));
   zdialog_add_widget(zd,"radio","warp","vbpw1",ZTX("warp"));
   zdialog_add_widget(zd,"hbox","hbp","vbpw2");
   zdialog_add_widget(zd,"label","labpr","hbp",Bradius,"space=5");
   zdialog_add_widget(zd,"spin","radius","hbp","1|400|1|100");
   zdialog_add_widget(zd,"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(zd,"radio",imageN,"hbim",labN);
   }

   zdialog_stuff(zd,"paint",1);                                                  //  paint button on
   zdialog_stuff(zd,"warp",0);                                                   //  warp button off
   zdialog_stuff(zd,"image1",1);                                                 //  initial image = 1st

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

   takeMouse(HDF_mousefunc,0);                                                   //  connect mouse function

   cc = E3pxm->ww * E3pxm->hh;                                                   //  allocate pixel map
   HDFpixmap = (char *) zmalloc(cc);
   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));
      HDFwarpy[imx] = (float *) zmalloc(ww * hh * sizeof(float));
   }

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

   zdialog_resize(zd,250,0);                                                     //  stretch a bit
   zdialog_run(zd,HDF_adjust_dialog_event,"-10/20");                             //  run dialog, parallel
   zdialog_wait(zd);                                                             //  wait for completion
   zdialog_free(zd);

   return;
}


//  dialog event and completion callback function

int HDF_adjust_dialog_event(zdialog *zd, cchar *event)
{
   int      imx, nn;

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat)                                                                //  dialog finish
   {
      zdialog_destroy(zd);                                                       //  prevent double [cancel]            15.02
      freeMouse();                                                               //  disconnect mouse function
      signal_thread();
      wrapup_thread(8);
      if (zd->zstat == 1) HDFstat = 1;
      else HDFstat = 0;
      if (HDFstat == 1) cim_trim();                                              //  cut-off edges
      HDFmode = 0;
      zfree(HDFpixmap);                                                          //  free pixel map
      for (imx = 0; imx < cimNF; imx++) {
         zfree(HDFwarpx[imx]);                                                   //  free warp memory
         zfree(HDFwarpy[imx]);
      }
   }

   if (strmatch(event,"paint")) {                                                //  set paint mode
      zdialog_fetch(zd,"paint",nn);
      if (! nn) return 1;
      HDFmode = 0;
      gdk_window_set_cursor(gdkwin,0);                                           //  no drag cursor
   }

   if (strmatch(event,"warp")) {                                                 //  set warp mode
      zdialog_fetch(zd,"warp",nn);
      if (! nn) return 1;
      HDFmode = 1;
      draw_mousecircle(0,0,0,1);                                                 //  stop mouse circle
      gdk_window_set_cursor(gdkwin,dragcursor);                                  //  set drag cursor
   }

   if (strmatchN(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 (strmatch(event,"radius"))                                                 //  change paint radius
      zdialog_fetch(zd,"radius",HDFradius);

   if (strmatch(event,"focus")) {                                                //  toggle mouse capture
      takeMouse(HDF_mousefunc,0);                                                //  connect mouse function
      if (HDFmode == 1)
         gdk_window_set_cursor(gdkwin,dragcursor);                               //  warp mode, drag cursor
      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()
{
   float       vpix1[4], *pix2, *pix3;
   int         imx, radius, rect, radius2, vstat1;
   int         mx, my, dx, dy, px3, py3;
   char        imageN[8] = "imageN";
   float       px1, py1;
   float       xoff, yoff, sintf[10], costf[10];
   int         ii, px, py, ww, hh;
   float       mag, dispx, dispy, d1, d2;
   PXM         *pxm1, *pxm2;

   if (LMclick || RMclick)                                                       //  mouse click
      return;                                                                    //  process zooms normally

   if (HDFmode == 0) goto paint;
   else if (HDFmode == 1) goto warp;
   else return;

paint:

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

   draw_mousecircle(Mxposn,Myposn,radius,0);                                     //  draw mouse selection circle

   if (! Mxdrag && ! Mydrag) return;

   mx = Mxdrag;                                                                  //  mouse drag
   my = Mydrag;
   Mxdrag = Mydrag = 0;

   if (mx < 0 || mx > E3pxm->ww-1 || my < 0 || my > E3pxm->hh-1)                 //  mouse outside image area
      return;

   for (imx = 0; imx < cimNF; imx++)                                             //  pre-calculate trig funcs
   {
      sintf[imx] = sinf(cimOffs[imx].tf);
      costf[imx] = cosf(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 > E3pxm->ww-1) continue;                                //  outside image
      if (py3 < 0 || py3 > E3pxm->hh-1) continue;

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

      imx = py3 * E3pxm->ww + 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) memcpy(pix3,vpix1,pixcc);
      else memset(pix3,0,pixcc);                                                 //  voided pixel
   }

   rect = 2 * radius;
   Fpaint3(mx-radius,my-radius,rect,rect);                                       //  update window
   draw_mousecircle(mx,my,radius,0);                                             //  draw mouse selection circle

   return;

warp:

   if (! Mxdrag && ! Mydrag) return;

   mx = Mxdrag;                                                                  //  mouse drag
   my = Mydrag;

   if (mx < 0 || mx > E3pxm->ww-1 || my < 0 || my > E3pxm->hh-1)                 //  mouse outside image area
      return;

   imx = my * E3pxm->ww + mx;                                                    //  if pixel has been painted,
   imx = HDFpixmap[imx];                                                         //    select corresp. image to warp
   if (imx == cimNF) return;                                                     //  else no action

   if (imx != HDFimage) {
      HDFimage = imx;                                                            //  update selected image and
      imageN[5] = '1' + imx;                                                     //    dialog radio button
      zdialog_stuff(HDFadjustzd,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;
   Mxdrag = Mydrag = 0;

   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) memcpy(pix2,vpix1,pixcc);
      else memset(pix2,0,pixcc);                                                 //  voided pixel
   }

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


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

void * HDF_adjust_thread(void *)
{
   void * HDF_combine_wthread(void *);

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

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(HDF_combine_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

      Fpaint2();
   }

   return 0;                                                                     //  not executed
}


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

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

   for (py3 = index+1; py3 < E3pxm->hh-1; py3 += NWT)                            //  step through output pixels
   for (px3 = 1; px3 < E3pxm->ww-1; px3++)
   {
      pix3 = PXMpix(E3pxm,px3,py3);

      imx = py3 * E3pxm->ww + 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) memcpy(pix3,vpix1,pixcc);
         else memset(pix3,0,pixcc);                                              //  voided pixel
      }

      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      STP_stat;                                                               //  1 = OK, 0 = failed or canceled
float    STP_initAlignSize = 160;                                                //  initial align image size
float    STP_imageIncrease = 1.6;                                                //  image size increase per align cycle
float    STP_sampSize = 10000;                                                   //  pixel sample size

float    STP_initSearchRange = 5.0;                                              //  initial search range, +/- pixels
float    STP_initSearchStep = 1.0;                                               //  initial search step, pixels
float    STP_initWarpRange = 2.0;                                                //  initial corner warp range
float    STP_initWarpStep = 1.0;                                                 //  initial corner warp step
float    STP_searchRange = 2.0;                                                  //  normal search range
float    STP_searchStep = 1.0;                                                   //  normal search step
float    STP_warpRange = 1.0;                                                    //  normal corner warp range
float    STP_warpStep = 0.67;                                                    //  normal corner warp step

void * STP_align_thread(void *);
void   STP_adjust();
void   STP_mousefunc();
void * STP_adjust_thread(void *);

editfunc    EFstp;                                                               //  edit function data


//  menu function

void m_STP(GtkWidget *, cchar *)
{
   char        **flist;
   int         imx, err, ww, hh;
   float       diffw, diffh;

   F1_help_topic = "stack_paint";                                                //  help topic

   if (checkpend("all")) return;

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

   cimNF = 0;
   cim_manualwarp = 0;

   flist = gallery_getfiles();                                                   //  select images to combine
   if (! flist) goto cleanup;

   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] = zstrdup(flist[imx]);                                        //  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[cimNF-1]);                                               //  curr_file = last in list
   if (err) goto cleanup;

   EFstp.menufunc = m_STP;
   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 (STP_stat != 1) goto cancel;

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

   CEF->Fmods++;                                                                 //  done
   CEF->Fsaved = 0;
   edit_done(0);
   goto cleanup;

cancel:
   edit_cancel(0);

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]);
   }

   *paneltext = 0;
   return;
}


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

void * STP_align_thread(void *)
{
   int         imx, im1, im2, ww, hh, ii, nn;
   float       R, maxtf, mintf, midtf;
   float       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 = 1;
   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 = STP_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 = STP_initSearchRange;                                      //  initial align search range
      cimSearchStep = STP_initSearchStep;                                        //  initial align search step
      cimWarpRange = STP_initWarpRange;                                          //  initial align corner warp range
      cimWarpStep = STP_initWarpStep;                                            //  initial align corner warp step
      cimSampSize = STP_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);

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

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

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

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

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

         R = STP_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 = STP_searchRange;                                       //  align search range
         cimSearchStep = STP_searchStep;                                         //  align search step size
         cimWarpRange = STP_warpRange;                                           //  align corner warp range
         cimWarpStep = STP_warpStep;                                             //  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 * sinf(toff);
      dyoff = xoff * sinf(toff);
      cimOffs[im2].xf -= dxoff;
      cimOffs[im2].yf += dyoff;
   }

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


//  paint output image

int      STP_image;                                                              //  current image (0 based)
int      STP_radius;                                                             //  paint mode radius
char     *STP_pixmap = 0;                                                        //  map input image per output pixel


void STP_adjust()
{
   zdialog  *zd;
   char     imageN[8] = "imageN", labN[4] = "0";
   int      cc, imx;

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

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

   zd = zdialog_new(ZTX("Select and Paint Image"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbim","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labim","hbim",Bimage,"space=5");
   zdialog_add_widget(zd,"hbox","hbmr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labr","hbmr",Bradius,"space=5");
   zdialog_add_widget(zd,"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(zd,"radio",imageN,"hbim",labN);
   }

   zdialog_stuff(zd,"image1",1);                                                 //  initial image = 1st

   STP_image = 0;                                                                //  initial image
   STP_radius = 100;                                                             //  paint radius

   takeMouse(STP_mousefunc,0);                                                   //  connect mouse function

   cc = E3pxm->ww * E3pxm->hh;                                                   //  allocate pixel map
   STP_pixmap = (char *) zmalloc(cc);
   memset(STP_pixmap,cimNF,cc);                                                  //  initial state, blend all images

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

   zdialog_resize(zd,200,0);
   zdialog_run(zd,STP_adjust_dialog_event,"-10/20");                             //  run dialog, parallel
   zdialog_wait(zd);                                                             //  wait for completion
   zdialog_free(zd);

   return;
}


//  dialog event and completion callback function

int STP_adjust_dialog_event(zdialog *zd, cchar *event)
{
   int      nn;

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat)                                                                //  dialog finish
   {
      zdialog_destroy(zd);                                                       //  prevent double [cancel]            15.02
      freeMouse();                                                               //  disconnect mouse function
      signal_thread();
      wrapup_thread(8);
      if (zd->zstat == 1) STP_stat = 1;
      else STP_stat = 0;
      if (STP_stat == 1) cim_trim();                                             //  cut-off edges
      zfree(STP_pixmap);                                                         //  free pixel map
   }

   if (strmatchN(event,"image",5)) {                                             //  image radio button
      nn = event[5] - '0';                                                       //  1 to cimNF
      if (nn > 0 && nn <= cimNF)
         STP_image = nn - 1;                                                     //  0 to cimNF-1
      signal_thread();
   }

   if (strmatch(event,"radius"))                                                 //  change paint radius
      zdialog_fetch(zd,"radius",STP_radius);

   if (strmatch(event,"focus")) {                                                //  toggle mouse capture
      takeMouse(STP_mousefunc,0);                                                //  connect mouse function
      signal_thread();
   }

   return 1;
}


//  STP dialog mouse function
//  during drag, selected image >> STP_pixmap (within paint radius) >> E3

void STP_mousefunc()
{
   float       vpix1[4], *pix3;
   int         imx, radius, rect, radius2, vstat1;
   int         mx, my, dx, dy, px3, py3;
   float       px1, py1;
   float       xoff, yoff, sintf[10], costf[10];

   radius = STP_radius;                                                          //  paintbrush radius
   radius2 = radius * radius;

   rect = 2 * radius;                                                            //  draw mouse selection circle
   draw_mousecircle(Mxposn,Myposn,radius,0);

   if (LMclick || RMclick)                                                       //  mouse click
      return;                                                                    //  handle zoom normally

   if (! Mxdrag && ! Mydrag) return;

   mx = Mxdrag;                                                                  //  mouse drag
   my = Mydrag;
   Mxdrag = Mydrag = 0;

   if (mx < 0 || mx > E3pxm->ww-1 || my < 0 || my > E3pxm->hh-1)                 //  mouse outside image area
      return;

   for (imx = 0; imx < cimNF; imx++)                                             //  pre-calculate trig funcs
   {
      sintf[imx] = sinf(cimOffs[imx].tf);
      costf[imx] = cosf(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 > E3pxm->ww-1) continue;                                //  outside image
      if (py3 < 0 || py3 > E3pxm->hh-1) continue;

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

      imx = py3 * E3pxm->ww + px3;                                               //  update pixmap to selected image
      STP_pixmap[imx] = STP_image;

      imx = STP_image;
      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) memcpy(pix3,vpix1,pixcc);
      else memset(pix3,0,pixcc);                                                 //  voided pixel
   }

   Fpaint3(mx-radius,my-radius,rect,rect);                                       //  update window
   draw_mousecircle(mx,my,radius,0);                                             //  draw mouse selection circle

   return;
}


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

void * STP_adjust_thread(void *)
{
   void * STP_combine_wthread(void *);

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

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

      Fpaint2();
   }

   return 0;                                                                     //  not executed
}


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

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

   for (py3 = index+1; py3 < E3pxm->hh-1; py3 += NWT)                            //  step through output pixels
   for (px3 = 1; px3 < E3pxm->ww-1; px3++)
   {
      pix3 = PXMpix(E3pxm,px3,py3);

      imx = py3 * E3pxm->ww + px3;
      imx = STP_pixmap[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) memcpy(pix3,vpix1,pixcc);
         else memset(pix3,0,pixcc);                                              //  voided pixel
      }

      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.

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

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

float    STN_initSearchRange = 5.0;                                              //  initial search range, +/- pixels
float    STN_initSearchStep = 1.0;                                               //  initial search step, pixels
float    STN_initWarpRange = 2.0;                                                //  initial corner warp range
float    STN_initWarpStep = 1.0;                                                 //  initial corner warp step
float    STN_searchRange = 2.0;                                                  //  normal search range
float    STN_searchStep = 1.0;                                                   //  normal search step
float    STN_warpRange = 1.0;                                                    //  normal corner warp range
float    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_adjust();
void * STN_adjust_thread(void *);

editfunc    EFstn;                                                               //  edit function data


//  menu function

void m_STN(GtkWidget *, cchar *)
{
   char        **flist;
   int         imx, err, ww, hh;
   float       diffw, diffh;

   F1_help_topic = "stack_noise";                                                //  help topic

   if (checkpend("all")) return;

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

   cimNF = 0;
   cim_manualwarp = 0;

   flist = gallery_getfiles();                                                   //  select images to combine
   if (! flist) goto cleanup;

   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] = zstrdup(flist[imx]);                                        //  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[cimNF-1]);                                               //  curr_file = last in list
   if (err) goto cleanup;

   EFstn.menufunc = m_STN;
   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_adjust();                                                                 //  combine images based on user inputs
   if (STN_stat != 1) goto cancel;

   CEF->Fmods++;                                                                 //  done
   CEF->Fsaved = 0;
   edit_done(0);
   goto cleanup;

cancel:
   edit_cancel(0);

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]);
   }

   *paneltext = 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 *)
{
   int         imx, im1, im2, ww, hh, ii, nn;
   float       R;

   Fzoom = 0;                                                                    //  fit to window if big
   Fblowup = 1;                                                                  //  scale up to window if small
   Ffuncbusy = 1;
   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);

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

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

         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
      }

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

   memset(&cimOffs[0],0,sizeof(cimoffs));

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


//  change pixel combination according to user input

void STN_adjust()
{
   zdialog     *zd;

   int STN_adjust_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",ZTX("use average"),"space=3");
   zdialog_add_widget(zd,"radio","median","hb1",ZTX("use median"),"space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","exlow","hb2",ZTX("omit low pixel"),"space=3");
   zdialog_add_widget(zd,"check","exhigh","hb2",ZTX("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_adjust_thread,0);                                            //  start working thread
   signal_thread();

   zdialog_resize(zd,250,0);
   zdialog_run(zd,STN_adjust_dialog_event,"-10/20");                             //  run dialog, parallel
   zdialog_wait(zd);                                                             //  wait for completion
   zdialog_free(zd);

   return;
}


//  dialog event and completion callback function

int STN_adjust_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat) {                                                              //  dialog finish
      zdialog_destroy(zd);                                                       //  prevent double [cancel]            15.02
      if (zd->zstat == 1) STN_stat = 1;
      else STN_stat = 0;
      wrapup_thread(8);
      if (STN_stat == 1) cim_trim();                                             //  trim edges
   }

   if (strmatch(event,"average")) {
      zdialog_fetch(zd,"average",STN_average);
      signal_thread();
   }

   if (strmatch(event,"median")) {
      zdialog_fetch(zd,"median",STN_median);
      signal_thread();
   }

   if (strmatch(event,"exlow")) {
      zdialog_fetch(zd,"exlow",STN_exlow);
      signal_thread();
   }

   if (strmatch(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_adjust_thread(void *)
{
   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,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed
}


//  worker thread

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

   //    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] = sinf(cimOffs[imx].tf);
      costf[imx] = cosf(cimOffs[imx].tf);
   }

   for (py3 = index+1; py3 < E3pxm->hh-1; py3 += NWT)                            //  step through output pixels
   for (px3 = 1; px3 < E3pxm->ww-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(E3pxm,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 horizontally

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

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

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

float panPreAlignBlend = 0.20;                                                   //  pre-align blend width * ww
float panInitBlend = 0.20;                                                       //  initial blend width during auto-align
float panFinalBlend = 0.05;                                                      //  final blend width * ww
float panBlendDecrease = 0.7;                                                    //  blend width reduction per align cycle

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

float pano_adjust_RGB[4][3];                                                     //  user color adjustments per image
int   pano_mousewarp = 0;                                                        //  use mouse to warp/align images
int   pano_currimage = 0;                                                        //  curr. image for color/warp adjustments
float *panowarpx[4], *panowarpy[4];                                              //  warp memory, pixel displacements
PXM   *cimPXMwdup[4];                                                            //  copies of images before mouse warps

void  pano_prealign();                                                           //  manual pre-align
void  pano_align();                                                              //  auto fine-align
void  pano_adjust();                                                             //  user color adjust

void  panowarp_mousefunc();                                                      //  image warp mouse function 
void  panowarp_dowarps(int imx);

editfunc EFpano;                                                                 //  edit function data


//  menu function

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

   F1_help_topic = "panorama";                                                   //  help topic

   if (checkpend("all")) return;

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

   flist = gallery_getfiles();                                                   //  select images to combine
   if (! flist) goto cleanup;

   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] = zstrdup(flist[imx]);                                        //  set up file list

   if (! cim_load_files()) goto cleanup;                                         //  load and check all files

   free_resources();                                                             //  ready to commit

   err = f_open(cimFile[cimNF-1]);                                               //  curr_file = last in list
   if (err) goto cleanup;

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

   cimShowAll = 1;                                                               //  for cim_show_images(), show all
   cimPano = 1;                                                                  //  horizontal pano mode
   cimPanoV = 0;

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

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

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

   CEF->Fmods++;                                                                 //  done
   CEF->Fsaved = 0;
   edit_done(0);
   goto cleanup;

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

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]);
   }

   *paneltext = 0;
   return;
}


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

void pano_prealign()
{
   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[1] = { exif_focal_length_key };
   cchar       *lens_source;
   char        *pp[1] = { 0 };
   float       temp;

   cchar  *align_mess = ZTX("Drag images into rough alignment.\n"
                            "To rotate, drag from lower edge.");
   cchar  *scan_mess = ZTX("no curve (scanned image)");
   cchar  *search_mess = ZTX("Search for lens mm");
   cchar  *save_mess = ZTX("Save lens mm → image EXIF");

   err = 1;
   lens_source = "NO EXIF";
   exif_get(curr_file,exifkey,pp,1);                                             //  get lens mm from EXIF if available
   if (pp[0]) err = convSF(pp[0], temp, 20, 1000);
   if (! err) {
      lens_mm = temp;
      lens_source = "(EXIF)";
   }

   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

/*
       _________________________________________
      |        Pre-align Images                 |
      |                                         |
      |  Drag images into rough alignment.      |
      |  To rotate, drag from lower edge.       |
      |                                         |
      | [ 35.5 |-+] lens mm (EXIF)              |
      | [x] no curve (scanned image)            |
      | [x] no auto warp                        |
      | [x] manual align                        |                                //  16.01
      | [Resize] resize window                  |
      | [Search] Search for lens mm             |
      | [Save] Save lens mm -> image EXIF       |
      |                                         |
      |                     [Proceed] [Cancel]  |
      |_________________________________________|

*/

   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=3");
   zdialog_add_widget(panozd,"hbox","hb1","dialog",0);
   zdialog_add_widget(panozd,"spin","spmm","hb1","20|999|0.1|35","space=5");
   zdialog_add_widget(panozd,"label","labmm","hb1",ZTX("lens mm"));
   zdialog_add_widget(panozd,"label","labsorc","hb1","","space=5");
   zdialog_add_widget(panozd,"hbox","hbnc","dialog");
   zdialog_add_widget(panozd,"check","nocurve","hbnc",scan_mess,"space=5");
   zdialog_add_widget(panozd,"hbox","hbmw","dialog");
   zdialog_add_widget(panozd,"check","manwarp","hbmw",ZTX("no auto warp"),"space=5");
   zdialog_add_widget(panozd,"hbox","hbma","dialog");                            //  16.01
   zdialog_add_widget(panozd,"check","manalign","hbma",ZTX("manual align"),"space=5");
   zdialog_add_widget(panozd,"hbox","hb5","dialog");
   zdialog_add_widget(panozd,"button","resize","hb5",ZTX("Resize"),"space=5");
   zdialog_add_widget(panozd,"label","labsiz","hb5",ZTX("resize window"),"space=5");
   zdialog_add_widget(panozd,"hbox","hb6","dialog");
   zdialog_add_widget(panozd,"button","search","hb6",Bsearch,"space=5");
   zdialog_add_widget(panozd,"label","labsearch","hb6",search_mess,"space=5");
   zdialog_add_widget(panozd,"hbox","hb7","dialog");
   zdialog_add_widget(panozd,"button","save","hb7",Bsave,"space=5");
   zdialog_add_widget(panozd,"label","labsave","hb7",save_mess,"space=5");

   zdialog_add_ttip(panozd,"manwarp",ZTX("do not warp images during auto-alignment"));

   zdialog_stuff(panozd,"spmm",lens_mm);                                         //  stuff lens data
   zdialog_stuff(panozd,"labsorc",lens_source);                                  //  show source of lens data
   zdialog_stuff(panozd,"nocurve",cimPanoNC);
   zdialog_stuff(panozd,"manwarp",0);

   panStat = -1;                                                                 //  busy status
   gdk_window_set_cursor(gdkwin,dragcursor);                                     //  set drag cursor
   zdialog_run(panozd,pano_prealign_event,"-10/20");                             //  start dialog
   start_thread(pano_prealign_thread,0);                                         //  start working thread
   zdialog_wait(panozd);                                                         //  wait for dialog completion
   zdialog_free(panozd);                                                         //  free dialog
   gdk_window_set_cursor(gdkwin,0);                                              //  restore normal cursor
   Fzoom = Fblowup = 0;
   return;
}


//  pre-align dialog event function

int pano_prealign_event(zdialog *zd, cchar *event)
{
   int      imx;
   float    overlap;
   cchar    *exifkey[1] = { exif_focal_length_key };
   cchar    *exifdata[1];
   char     lensdata[8];

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (strmatch(event,"spmm"))
      zdialog_fetch(zd,"spmm",lens_mm);                                          //  get revised lens data

   if (strmatch(event,"manwarp"))                                                //  get auto/manual warp option
      zdialog_fetch(zd,"manwarp",cim_manualwarp);

   if (strmatch(event,"manalign"))                                               //  get auto/manual align option       16.01
      zdialog_fetch(zd,"manalign",cim_manualalign);

   if (strmatch(event,"nocurve"))
      zdialog_fetch(zd,"nocurve",cimPanoNC);                                     //  get "no-curve" option

   if (strmatch(event,"resize"))                                                 //  allocate new E3 image
      cim_show_images(1,0);

   if (strmatch(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 (strmatch(event,"save")) {                                                 //  put lens data into dialog
      zdialog_stuff(zd,"spmm",lens_mm);
      snprintf(lensdata,8,"%d",int(lens_mm));
      exifdata[0] = lensdata;
      for (imx = 0; imx < cimNF; imx++)                                          //  save lens mm in EXIF data
         exif_put(cimFile[imx],exifkey,exifdata,1);
   }

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

      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);
         if (overlap < panFinalBlend) {
            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

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;
   int         fchange, nc0;
   float       lens_mm0;
   float       dx, dy, t1, t2, dt;

   imm = ww = hh = rotate = xoff = yoff = 0;                                     //  stop compiler warnings

   lens_mm0 = lens_mm;                                                           //  to detect changes
   nc0 = cimPanoNC;

   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.02);                                                              //  logic simplified

      if (panStat == 2) {                                                        //  dialog search button
         panStat = -1;                                                           //  back to busy status
         pano_autolens();
      }

      if (panStat != -1) break;                                                  //  quit signal from dialog

      fchange = 0;
      if (lens_mm != lens_mm0)                                                   //  change in lens parameter
         fchange = 1;
      if (cimPanoNC != nc0) fchange = 1;                                         //  change in "no-curve" option

      if (fchange) {
         lens_mm0 = lens_mm;
         nc0 = cimPanoNC;

         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_KEY_Left)  cimOffs[imm].xf -= 0.5;                     //  adjust alignment offsets
         if (KBkey == GDK_KEY_Right) cimOffs[imm].xf += 0.5;
         if (KBkey == GDK_KEY_Up)    cimOffs[imm].yf -= 0.5;
         if (KBkey == GDK_KEY_Down)  cimOffs[imm].yf += 0.5;
         if (KBkey == GDK_KEY_r)     cimOffs[imm].tf += 0.0005;
         if (KBkey == GDK_KEY_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) && ! Fmousemain)                                    //  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
         }

         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;
            yoff = cimOffs[imm].yf;

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

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

      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

void pano_autolens()
{
   float       mm_range, xf_range, yf_range, tf_range;
   float       squeeze, xf_rfinal, rnum, matchB, matchlev;
   float       overlap, lens_mmB;
   int         imx, randcount = 0;
   cimoffs     offsetsB;

   overlap = cim_get_overlap(0,1,cimPXMs);
   if (overlap < 0.1) {
      zmessageACK(Mwin,ZTX("Too little overlap, cannot align"));
      return;
   }

   Ffuncbusy = 1;

   cimSampSize = 5000;
   cimNsearch = 0;

   mm_range = 0.1 * lens_mm;                                                     //  set initial search ranges
   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
   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 factor

      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
      cim_show_images(0,0);                                                      //  combine and show images

      squeeze = 0.97;                                                            //  search range reduction

      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

         snprintf(paneltext,200,"align: %d  match: %.5f  lens: %.1f",
                           ++cimNsearch, matchB, lens_mmB);

         if (sigdiff(matchlev,matchB,0.00001) > 0) {
            matchB = matchlev;                                                   //  save new best fit
            lens_mmB = lens_mm;                                                  //  alignment is better
            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
         zmainloop();
      }

      if (xf_range < xf_rfinal) goto done;                                       //  finished

      snprintf(paneltext,200,"align: %d  match: %.5f  lens: %.1f",
                        cimNsearch, matchB, lens_mmB);

      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
      xf_range = squeeze * xf_range;
      yf_range = squeeze * yf_range;
      tf_range = squeeze * tf_range;
      zmainloop();
   }

done:
   zfree(cimRedpix);
   cimRedpix = 0;

   lens_mm = lens_mmB;                                                           //  save best lens param found

   cimSampSize = panSampSize;                                                    //  restore
   Ffuncbusy = 0;
   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()
{
   int         imx, im1, im2, ww;
   float       R, dx, dy, dt;
   float       overlap;
   cimoffs     offsets0;

   Fzoom = 0;                                                                    //  scale E3 to fit window
   Fblowup = 1;                                                                  //  magnify small image to window size
   Ffuncbusy = 1;

   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;

   if (cim_manualalign) cimScale = 1.0;                                          //  manual alignment, full size        16.01

   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
         if (overlap < panFinalBlend-2) {
            zmessageACK(Mwin,ZTX("Too little overlap, cannot align"));
            goto fail;
         }
         
         if (cim_manualalign) cim_show_images(0,0);                              //  16.01
         else {
            cim_get_redpix(im1);                                                 //  get high-contrast pixels
            cim_show_images(0,0);                                                //  show with 50/50 blend in overlaps
            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                               16.02

      R = panImageIncrease;                                                      //  next larger image size
      cimScale = cimScale * R;
      if (cimScale > 0.85) {                                                     //  if close to end, jump to end       16.02
         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 adjust)
   Fzoom = Fblowup = 0;
   Ffuncbusy = 0;
   cim_show_images(0,0);
   return;
}


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

void pano_adjust()
{
   int   pano_adjust_event(zdialog *zd, cchar *event);                           //  dialog event function

   cchar    *adjusttitle = ZTX("Match Brightness and Color");
   char     imageN[8] = "imageN";
   int      ww, hh, cc, imx;

   for (imx = 0; imx < cimNF; imx++) {                                           //  neutral color adjustments
      pano_adjust_RGB[imx][0] = 100;
      pano_adjust_RGB[imx][1] = 100;
      pano_adjust_RGB[imx][2] = 100;
   }

   for (imx = 0; imx < cimNF; imx++) {                                           //  allocate warp memory
      ww = cimPXMw[imx]->ww;
      hh = cimPXMw[imx]->hh;
      cc = ww * hh * sizeof(float);
      panowarpx[imx] = (float *) zmalloc(cc);
      panowarpy[imx] = (float *) zmalloc(cc);
      memset(panowarpx[imx],0,cc);
      memset(panowarpy[imx],0,cc);
      cimPXMwdup[imx] = PXM_copy(cimPXMw[imx]);
   }

   cimBlend = 1;                                                                 //  init. blend width

/***
       _________________________________________
      |      Match Brightness and Color         |
      |                                         |
      |  Select image (o) (o) (o) (o)           |
      |                                         |
      |     red         green       blue        |
      |   [_____-|+]  [_____-|+]  [_____-|+]    |
      |   brightness [___-|+]  [apply]          |
      |   [auto color]  [file color]            |
      |   blend width [___] [apply]             |
      |   [x] mouse warp                        |
      |   [flatten] curved image                |
      |                                         |
      |                        [done]  [cancel] |
      |_________________________________________|

***/

   panozd = zdialog_new(adjusttitle,Mwin,Bdone,Bcancel,null);

   zdialog_add_widget(panozd,"hbox","hbim","dialog",0,"space=3");
   zdialog_add_widget(panozd,"label","labim","hbim",ZTX("Select image"),"space=5");
   zdialog_add_widget(panozd,"hbox","hbc1","dialog",0,"homog");
   zdialog_add_widget(panozd,"label","labred","hbc1",Bred);
   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");
   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=3");
   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","rgbapp","hbbri",Bapply,"space=10");
   zdialog_add_widget(panozd,"hsep","hsep","dialog",0,"space=3");
   zdialog_add_widget(panozd,"hbox","hbc3","dialog",0,"space=3");
   zdialog_add_widget(panozd,"button","auto","hbc3",ZTX("auto color"),"space=5");
   zdialog_add_widget(panozd,"button","file","hbc3",ZTX("file color"),"space=5");
   zdialog_add_widget(panozd,"hbox","hbblen","dialog",0,"space=3");
   zdialog_add_widget(panozd,"label","labbl","hbblen",Bblendwidth,"space=5");
   zdialog_add_widget(panozd,"spin","blend","hbblen","1|999|1|1");
   zdialog_add_widget(panozd,"button","apply","hbblen",Bapply,"space=15");
   zdialog_add_widget(panozd,"hbox","hbwarp","dialog",0,"space=3");
   zdialog_add_widget(panozd,"check","mousewarp","hbwarp",ZTX("mouse warp"),"space=3");
   zdialog_add_widget(panozd,"hbox","hbflat","dialog",0,"space=3");
   zdialog_add_widget(panozd,"button","flatten","hbflat",Bflatten,"space=3");
   zdialog_add_widget(panozd,"label","labflat","hbflat",ZTX("curved image"),"space=5");

   for (imx = 0; imx < cimNF; imx++) {                                           //  add radio button per image
      imageN[5] = '0' + imx;
      zdialog_add_widget(panozd,"radio",imageN,"hbim");
   }

   zdialog_resize(panozd,300,0);

   zdialog_stuff(panozd,"image0",1);                                             //  pre-select 1st image
   pano_currimage = 0;
   zdialog_stuff(panozd,"mousewarp",0);                                          //  default mouse warp off

   panStat = -1;                                                                 //  busy status
   zdialog_run(panozd,pano_adjust_event,"-10/20");                               //  run dialog, parallel
   zdialog_wait(panozd);                                                         //  wait for dialog completion
   zdialog_free(panozd);                                                         //  free dialog

   return;
}


//  dialog event function

int pano_adjust_event(zdialog *zd, cchar *event)
{
   char        imageN[8] = "imageN";
   float       R, G, B, R1, G1, B1;
   int         imS, imx, im1, im2;
   int         ww, hh, px, py, nn;
   float       bright, bright2, *pixel;

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat)                                                                //  dialog complete
   {
      if (zd->zstat == 1) panStat = 1;                                           //  done
      else panStat = 0;                                                          //  cancel
      freeMouse();
      for (imx = 0; imx < cimNF; imx++)                                          //  free memory
         PXM_free(cimPXMwdup[imx]);
      return 0;
   }

   imS = pano_currimage;                                                         //  last image selected

   zdialog_fetch(zd,"red",R);                                                    //  get color settings
   zdialog_fetch(zd,"green",G);
   zdialog_fetch(zd,"blue",B);
   zdialog_fetch(zd,"bright",bright);

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

   if (imx != imS) {                                                             //  new image selected
      pano_adjust_RGB[imS][0] = R;                                               //  save color settings for old image
      pano_adjust_RGB[imS][1] = G;
      pano_adjust_RGB[imS][2] = B;
      imS = pano_currimage = imx;
      R = pano_adjust_RGB[imS][0];                                               //  get settings for new image
      G = pano_adjust_RGB[imS][1];
      B = pano_adjust_RGB[imS][2];
      zdialog_stuff(zd,"red",R);
      zdialog_stuff(zd,"green",G);
      zdialog_stuff(zd,"blue",B);
      bright = (R + G + B) / 3;
      zdialog_stuff(zd,"bright",bright);
   }

   if (strstr("red green blue",event)) {                                         //  new RGB value
      bright = (R + G + B) / 3;
      zdialog_stuff(zd,"bright",bright);                                         //  matching brightness
   }

   if (strmatch(event,"bright")) {                                               //  brightness change
      bright2 = (R + G + B) / 3;
      bright2 = bright / bright2;
      R = R * bright2;                                                           //  matching RGB
      G = G * bright2;
      B = B * bright2;
      zdialog_stuff(zd,"red",R);
      zdialog_stuff(zd,"green",G);
      zdialog_stuff(zd,"blue",B);
   }

   if (strmatch(event,"rgbapp"))                                                 //  apply color & brightness changes
   {
      R = R / 100;                                                               //  normalize 0.5 ... 2.0
      G = G / 100;
      B = B / 100;

      cim_warp_image_pano(imS,0);                                                //  refresh cimPXMw from cimPXMs

      ww = cimPXMw[imS]->ww;
      hh = cimPXMw[imS]->hh;

      for (py = 0; py < hh; py++)                                                //  loop all image pixels
      for (px = 0; px < ww; px++)
      {
         pixel = PXMpix(cimPXMw[imS],px,py);
         R1 = R * pixel[0];                                                      //  apply color factors
         G1 = G * pixel[1];
         B1 = B * pixel[2];

         if (R1 > 255.9 || G1 > 255.9 || B1 > 255.9) {
            bright = R1;                                                         //  avoid overflow
            if (G1 > bright) bright = G1;
            if (B1 > bright) bright = B1;
            bright = 255.9 / bright;
            R1 = R1 * bright;
            G1 = G1 * bright;
            B1 = B1 * bright;
         }

         pixel[0] = R1;
         pixel[1] = G1;
         pixel[2] = B1;
      }

      cimBlend = 1;
      zdialog_stuff(zd,"blend",cimBlend);
      cim_show_images(0,0);                                                      //  combine and show with 50/50 blend
   }

   if (strmatch(event,"auto"))                                                   //  auto match color of selected image
   {
      for (im1 = imS; 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_pano(cimPXMw[im1],1);                                 //  image im1 << profile 1
         cim_adjust_colors_pano(cimPXMw[im2],2);                                 //  image im2 << profile 2
         for (imx = im1-1; imx >= imS; imx--)
            cim_adjust_colors_pano(cimPXMw[imx],1);
      }

      for (im1 = imS-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_pano(cimPXMw[im1],1);                                 //  image im1 << profile 1
         cim_adjust_colors_pano(cimPXMw[im2],2);                                 //  image im2 << profile 2
         for (imx = im2+1; imx < cimNF; imx++)
            cim_adjust_colors_pano(cimPXMw[imx],2);
      }

      cimBlend = 1;
      zdialog_stuff(zd,"blend",cimBlend);
      cim_show_images(0,0);
   }

   if (strmatch(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);
         PXM_free(cimPXMwdup[imx]);
         cimPXMwdup[imx] = PXM_copy(cimPXMw[imx]);
         panowarp_dowarps(imx);                                                  //  re-apply mouse warps
      }

      cimBlend = 1;
      zdialog_stuff(zd,"blend",cimBlend);
      cim_show_images(0,0);
   }

   if (strmatch(event,"apply"))                                                  //  apply new blend width
   {
      zdialog_fetch(zd,"blend",cimBlend);                                        //  can be zero
      cim_show_images(0,1);                                                      //  show with gradual blend
   }

   if (strmatch(event,"mousewarp")) {                                            //  engage mouse warp/align
      zdialog_fetch(zd,"mousewarp",pano_mousewarp);
      if (pano_mousewarp)
         takeMouse(panowarp_mousefunc,dragcursor);                               //  connect mouse function
      else freeMouse();
   }

   if (strmatch(event,"flatten")) {
      cim_uncurve_image();
      Fpaint2();
   }

   return 1;
}


//  panorama dialog mouse warp function
//  for selected image, cimPXMs >> warp >> cimPXMw >> E3

void panowarp_mousefunc() 
{
   int         imx;
   int         mx, my, dx, dy;
   int         ii, px, py, ww, hh;
   float       mag, dispx, dispy, d1, d2;
   PXM         *pxm;

   if (LMclick || RMclick) return;                                               //  mouse click
   if (! Mxdrag && ! Mydrag) return;
   if (! pano_mousewarp) return;

   mx = Mxdrag;                                                                  //  mouse drag
   my = Mydrag;

   if (mx < 0 || mx > E3pxm->ww-1 || my < 0 || my > E3pxm->hh-1)                 //  mouse outside image area
      return;

   imx = pano_currimage;

   pxm = cimPXMw[imx];                                                           //  output image
   ww = pxm->ww;
   hh = pxm->hh;

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

   mx = mx - cimOffs[imx].xf;                                                    //  mouse position relative
   my = my - cimOffs[imx].yf;                                                    //    to image origin

   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 = pow(mag,50);

      dispx = -dx * mag;                                                         //  displacement = drag * mag
      dispy = -dy * mag;

      ii = py * ww + px;
      panowarpx[imx][ii] += dispx;                                               //  add this drag to prior sum
      panowarpy[imx][ii] += dispy;
   }

   panowarp_dowarps(imx);                                                        //  do accumulated warps

   if (cimPanoV) cim_show_Vimages(0,0);                                          //  show images
   else cim_show_images(0,0);

   return;
}


//  warp the image using the accumulated mouse drags

void panowarp_dowarps(int imx)
{
   float       vpix1[4], *pix2;
   int         ii, px, py, ww, hh, vstat1;
   float       dispx, dispy;
   PXM         *pxm1, *pxm2;

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

   for (py = 0; py < hh; py++)                                                   //  process all output pixels
   for (px = 0; px < ww; px++)
   {
      ii = py * ww + px;

      dispx = panowarpx[imx][ii];
      dispy = panowarpy[imx][ii];         //////////

      vstat1 = vpixel(pxm1,px+dispx,py+dispy,vpix1);                             //  input virtual pixel
      pix2 = PXMpix(pxm2,px,py);                                                 //  output pixel
      if (vstat1) memcpy(pix2,vpix1,pixcc);
      else memset(pix2,0,pixcc);                                                 //  voided pixel
   }

   return;
}


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

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

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

void  vpano_prealign();                                                          //  manual pre-align
void  vpano_align();                                                             //  auto fine-align
void  vpano_adjust();                                                            //  user color adjust

editfunc    EFvpano;                                                             //  edit function data


//  menu function

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

   F1_help_topic = "panorama";                                                   //  help topic

   if (checkpend("all")) return;

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

   cimNF = 0;
   cim_manualwarp = 0;
   pano_mousewarp = 0;
   cim_manualalign = 0;                                                          //  16.01

   flist = gallery_getfiles();                                                   //  select images to combine
   if (! flist) goto cleanup;

   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] = zstrdup(flist[imx]);                                        //  set up file list

   if (! cim_load_files()) goto cleanup;                                         //  load and check all files

   free_resources();                                                             //  ready to commit

   err = f_open(cimFile[cimNF-1]);                                               //  curr_file = last in list
   if (err) goto cleanup;

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

   cimShowAll = 1;                                                               //  for cim_show_images(), show all
   cimPano = 0;                                                                  //  vertical pano mode
   cimPanoV = 1;

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

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

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

   CEF->Fmods++;                                                                 //  done
   CEF->Fsaved = 0;
   edit_done(0);
   goto cleanup;

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

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]);
   }

   *paneltext = 0;
   return;
}


//  perform manual pre-align of all images
//  returns alignment data in cimOffs[*]
//  lens_mm 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[1] = { exif_focal_length_key };
   cchar       *lens_source;
   char        *pp[1] = { 0 };
   float       temp;

   cchar  *align_mess = ZTX("Drag images into rough alignment.\n"
                            "To rotate, drag from right edge.");
   cchar  *scan_mess = ZTX("no curve (scanned image)");
   cchar  *search_mess = ZTX("Search for lens mm");
   cchar  *save_mess = ZTX("Save lens mm → image EXIF");

   err = 1;
   lens_source = "NO EXIF";
   exif_get(curr_file,exifkey,pp,1);                                             //  get lens mm from EXIF if available
   if (pp[0]) err = convSF(pp[0], temp, 20, 1000);
   if (! err) {
      lens_mm = temp;
      lens_source = "(EXIF)";
   }

   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

/*
       _________________________________________
      |        Pre-align Images                 |
      |                                         |
      |  Drag images into rough alignment.      |
      |  To rotate, drag from right edge.       |
      |                                         |
      | [ 35.5 |-+] lens mm (EXIF)              |
      | [x] no curve (scanned image)            |
      | [x] no auto warp                        |
      | [x] manual align                        |                                //  16.01
      | [Resize] resize window                  |
      | [Search] Search for lens mm             |
      | [Save] Save lens mm -> image EXIF       |
      |                                         |
      |                     [Proceed] [Cancel]  |
      |_________________________________________|

*/

   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=3");
   zdialog_add_widget(panozd,"hbox","hb1","dialog",0);
   zdialog_add_widget(panozd,"spin","spmm","hb1","20|999|0.1|35","space=5");
   zdialog_add_widget(panozd,"label","labmm","hb1",ZTX("lens mm"));
   zdialog_add_widget(panozd,"label","labsorc","hb1","","space=5");
   zdialog_add_widget(panozd,"hbox","hbnc","dialog");
   zdialog_add_widget(panozd,"check","nocurve","hbnc",scan_mess,"space=5");
   zdialog_add_widget(panozd,"hbox","hbmw","dialog");
   zdialog_add_widget(panozd,"check","manwarp","hbmw",ZTX("no auto warp"),"space=5");
   zdialog_add_widget(panozd,"hbox","hbma","dialog");                            //  16.01
   zdialog_add_widget(panozd,"check","manalign","hbma",ZTX("manual align"),"space=5");
   zdialog_add_widget(panozd,"hbox","hb5","dialog");
   zdialog_add_widget(panozd,"button","resize","hb5",ZTX("Resize"),"space=5");
   zdialog_add_widget(panozd,"label","labsiz","hb5",ZTX("resize window"),"space=5");
   zdialog_add_widget(panozd,"hbox","hb6","dialog");
   zdialog_add_widget(panozd,"button","search","hb6",Bsearch,"space=5");
   zdialog_add_widget(panozd,"label","labsearch","hb6",search_mess,"space=5");
   zdialog_add_widget(panozd,"hbox","hb7","dialog");
   zdialog_add_widget(panozd,"button","save","hb7",Bsave,"space=5");
   zdialog_add_widget(panozd,"label","labsave","hb7",save_mess,"space=5");

   zdialog_add_ttip(panozd,"manwarp",ZTX("do not warp images during auto-alignment"));

   zdialog_stuff(panozd,"spmm",lens_mm);                                         //  stuff lens data
   zdialog_stuff(panozd,"labsorc",lens_source);                                  //  show source of lens data
   zdialog_stuff(panozd,"nocurve",cimPanoNC);
   zdialog_stuff(panozd,"manwarp",0);

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


//  pre-align dialog event function

int vpano_prealign_event(zdialog *zd, cchar *event)
{
   int      imx;
   float    overlap;
   cchar    *exifkey[1] = { exif_focal_length_key };
   cchar    *exifdata[1];
   char     lensdata[8];

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (strmatch(event,"spmm"))
      zdialog_fetch(zd,"spmm",lens_mm);                                          //  get revised lens data

   if (strmatch(event,"manwarp"))                                                //  get auto/manual warp option
      zdialog_fetch(zd,"manwarp",cim_manualwarp);

   if (strmatch(event,"manalign"))                                               //  get auto/manual align option       16.01
      zdialog_fetch(zd,"manalign",cim_manualalign);

   if (strmatch(event,"nocurve"))
      zdialog_fetch(zd,"nocurve",cimPanoNC);                                     //  get "no-curve" option

   if (strmatch(event,"resize"))                                                 //  allocate new E3 image
      cim_show_Vimages(1,0);

   if (strmatch(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 (strmatch(event,"save")) {                                                 //  put lens data into dialog
      zdialog_stuff(zd,"spmm",lens_mm);
      snprintf(lensdata,8,"%d",int(lens_mm));
      exifdata[0] = lensdata;
      for (imx = 0; imx < cimNF; imx++)                                          //  save lens mm in EXIF data
         exif_put(cimFile[imx],exifkey,exifdata,1);
   }

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

      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);
         if (overlap < panFinalBlend) {
            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 *)
{
   void   vpano_autolens();

   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;
   int         fchange, nc0;
   float       lens_mm0;
   float       dx, dy, t1, t2, dt;

   imm = ww = hh = rotate = xoff = yoff = 0;                                     //  stop compiler warnings

   lens_mm0 = lens_mm;                                                           //  to detect changes
   nc0 = cimPanoNC;

   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
         vpano_autolens();
      }

      if (panStat != -1) break;                                                  //  quit signal from dialog

      fchange = 0;
      if (lens_mm != lens_mm0)                                                   //  change in lens parameter
         fchange = 1;
      if (cimPanoNC != nc0) fchange = 1;                                         //  change in "no-curve" option

      if (fchange) {
         lens_mm0 = lens_mm;
         nc0 = cimPanoNC;

         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_KEY_Left)  cimOffs[imm].xf -= 0.5;                     //  adjust alignment offsets
         if (KBkey == GDK_KEY_Right) cimOffs[imm].xf += 0.5;
         if (KBkey == GDK_KEY_Up)    cimOffs[imm].yf -= 0.5;
         if (KBkey == GDK_KEY_Down)  cimOffs[imm].yf += 0.5;
         if (KBkey == GDK_KEY_r)     cimOffs[imm].tf += 0.0005;
         if (KBkey == GDK_KEY_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) && ! Fmousemain)                                    //  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;
            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
}


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

void vpano_autolens()
{
   float       mm_range, xf_range, yf_range, tf_range;
   float       squeeze, xf_rfinal, rnum, matchB, matchlev;
   float       overlap, lens_mmB;
   int         imx, randcount = 0;
   cimoffs     offsetsB;

   overlap = cim_get_overlap(0,1,cimPXMs);
   if (overlap < 0.1) {
      zmessageACK(Mwin,ZTX("Too little overlap, cannot align"));
      return;
   }

   Ffuncbusy = 1;

   cimSampSize = 5000;
   cimNsearch = 0;

   mm_range = 0.1 * lens_mm;                                                     //  set initial search ranges
   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
   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 factor

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

      cim_get_redpix(0);                                                         //  get high-contrast pixels
      cim_show_Vimages(0,0);                                                     //  combine and show images

      squeeze = 0.97;                                                            //  search range reduction

      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

         snprintf(paneltext,200,"align: %d  match: %.5f  lens: %.1f",
                           ++cimNsearch, matchB, lens_mmB);

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

         if (panStat != -1) goto done;                                           //  user kill
         zmainloop();
      }

      if (xf_range < xf_rfinal) goto done;                                       //  finished

      snprintf(paneltext,200,"align: %d  match: %.5f  lens: %.1f",
                        cimNsearch, matchB, lens_mmB);

      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
      xf_range = squeeze * xf_range;
      yf_range = squeeze * yf_range;
      tf_range = squeeze * tf_range;
      zmainloop();
   }

done:
   zfree(cimRedpix);
   cimRedpix = 0;

   lens_mm = lens_mmB;                                                           //  save best lens params found

   cimSampSize = panSampSize;                                                    //  restore
   Ffuncbusy = 0;
   cim_show_Vimages(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 vpano_align()
{
   int         imx, im1, im2, ww, hh;
   float       R, dx, dy, dt;
   float       overlap;
   cimoffs     offsets0;

   Fzoom = 0;                                                                    //  scale E3 to fit window
   Fblowup = 1;                                                                  //  magnify small image to window size
   Ffuncbusy = 1;

   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;

   if (cim_manualalign) cimScale = 1.0;                                          //  manual alignment, full size        16.01

   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
         if (overlap < panFinalBlend-2) {
            zmessageACK(Mwin,ZTX("Too little overlap, cannot align"));
            goto fail;
         }

         if (cim_manualalign) cim_show_Vimages(0,0);                             //  16.01
         else {
            cim_get_redpix(im1);                                                 //  get high-contrast pixels
            cim_show_Vimages(0,0);                                               //  show with 50/50 blend in overlaps
            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                               16.02

      R = panImageIncrease;                                                      //  next larger image size
      cimScale = cimScale * R;
      if (cimScale > 0.85) {                                                     //  if close to end, jump to end       16.02
         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 = 0;
   cimBlend = 1;                                                                 //  tiny blend (increase in adjust if wanted)
   cim_show_Vimages(0,0);
   return;
}


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

void vpano_adjust()
{
   int   vpano_adjust_event(zdialog *zd, cchar *event);                          //  dialog event function

   cchar    *adjusttitle = ZTX("Match Brightness and Color");
   char     imageN[8] = "imageN";
   int      ww, hh, cc, imx;

   for (imx = 0; imx < cimNF; imx++) {                                           //  neutral color adjustments
      pano_adjust_RGB[imx][0] = 100;
      pano_adjust_RGB[imx][1] = 100;
      pano_adjust_RGB[imx][2] = 100;
   }

   for (imx = 0; imx < cimNF; imx++) {                                           //  allocate warp memory
      ww = cimPXMw[imx]->ww;
      hh = cimPXMw[imx]->hh;
      cc = ww * hh * sizeof(float);
      panowarpx[imx] = (float *) zmalloc(cc);
      panowarpy[imx] = (float *) zmalloc(cc);
      memset(panowarpx[imx],0,cc);
      memset(panowarpy[imx],0,cc);
      cimPXMwdup[imx] = PXM_copy(cimPXMw[imx]);
   }

   cimBlend = 1;                                                                 //  init. blend width

/***
       _________________________________________
      |      Match Brightness and Color         |
      |                                         |
      |  Image (o) (o) (o) (o)                  |
      |                                         |
      |     red         green       blue        |
      |   [_____-|+]  [_____-|+]  [_____-|+]    |
      |   brightness [___-|+]  [apply]          |
      |   [auto color]  [file color]            |
      |   blend width [___] [apply]             |
      |   [x] mouse warp                        |
      |                                         |
      |                        [done]  [cancel] |
      |_________________________________________|

***/

   panozd = zdialog_new(adjusttitle,Mwin,Bdone,Bcancel,null);

   zdialog_add_widget(panozd,"hbox","hbim","dialog",0,"space=3");
   zdialog_add_widget(panozd,"label","labim","hbim",Bimage,"space=5");
   zdialog_add_widget(panozd,"hbox","hbc1","dialog",0,"homog");
   zdialog_add_widget(panozd,"label","labred","hbc1",Bred);
   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");
   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=3");
   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","rgbapp","hbbri",Bapply,"space=10");
   zdialog_add_widget(panozd,"hsep","hsep","dialog",0,"space=3");
   zdialog_add_widget(panozd,"hbox","hbc3","dialog",0,"space=3");
   zdialog_add_widget(panozd,"button","auto","hbc3",ZTX("auto color"),"space=5");
   zdialog_add_widget(panozd,"button","file","hbc3",ZTX("file color"),"space=5");
   zdialog_add_widget(panozd,"hbox","hbblen","dialog",0,"space=3");
   zdialog_add_widget(panozd,"label","labbl","hbblen",Bblendwidth,"space=5");
   zdialog_add_widget(panozd,"spin","blend","hbblen","1|999|1|1");
   zdialog_add_widget(panozd,"button","apply","hbblen",Bapply,"space=15");
   zdialog_add_widget(panozd,"hbox","hbwarp","dialog",0,"space=3");
   zdialog_add_widget(panozd,"check","mousewarp","hbwarp",ZTX("mouse warp"),"space=3");

   for (imx = 0; imx < cimNF; imx++) {                                           //  add radio button per image
      imageN[5] = '0' + imx;
      zdialog_add_widget(panozd,"radio",imageN,"hbim",0,"space=3");
   }

   zdialog_resize(panozd,300,0);

   zdialog_stuff(panozd,"image0",1);                                             //  pre-select 1st image
   pano_currimage = 0;
   zdialog_stuff(panozd,"mousewarp",0);                                          //  default mouse warp off

   panStat = -1;                                                                 //  busy status
   zdialog_run(panozd,vpano_adjust_event,"-10/20");                              //  run dialog, parallel
   zdialog_wait(panozd);                                                         //  wait for dialog completion
   zdialog_free(panozd);                                                         //  free dialog

   return;
}


//  dialog event function

int vpano_adjust_event(zdialog *zd, cchar *event)
{
   char        imageN[8] = "imageN";
   float       R, G, B, R1, G1, B1;
   int         imS, imx, im1, im2;
   int         ww, hh, px, py, nn;
   float       bright, bright2, *pixel;

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat)                                                                //  dialog complete
   {
      if (zd->zstat == 1) panStat = 1;                                           //  done
      else panStat = 0;                                                          //  cancel
      freeMouse();
      for (imx = 0; imx < cimNF; imx++)                                          //  free memory
         PXM_free(cimPXMwdup[imx]);
      return 0;
   }

   imS = pano_currimage;                                                         //  last image selected

   zdialog_fetch(zd,"red",R);                                                    //  get color settings
   zdialog_fetch(zd,"green",G);
   zdialog_fetch(zd,"blue",B);
   zdialog_fetch(zd,"bright",bright);

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

   if (imx != imS) {                                                             //  new image selected
      pano_adjust_RGB[imS][0] = R;                                               //  save color settings for old image
      pano_adjust_RGB[imS][1] = G;
      pano_adjust_RGB[imS][2] = B;
      imS = pano_currimage = imx;
      R = pano_adjust_RGB[imS][0];                                               //  get settings for new image
      G = pano_adjust_RGB[imS][1];
      B = pano_adjust_RGB[imS][2];
      zdialog_stuff(zd,"red",R);
      zdialog_stuff(zd,"green",G);
      zdialog_stuff(zd,"blue",B);
      bright = (R + G + B) / 3;
      zdialog_stuff(zd,"bright",bright);
   }

   if (strstr("red green blue",event)) {                                         //  new RGB value
      bright = (R + G + B) / 3;
      zdialog_stuff(zd,"bright",bright);                                         //  matching brightness
   }

   if (strmatch(event,"bright")) {                                               //  brightness change
      bright2 = (R + G + B) / 3;
      bright2 = bright / bright2;
      R = R * bright2;                                                           //  matching RGB
      G = G * bright2;
      B = B * bright2;
      zdialog_stuff(zd,"red",R);
      zdialog_stuff(zd,"green",G);
      zdialog_stuff(zd,"blue",B);
   }

   if (strmatch(event,"rgbapp"))                                                 //  apply color & brightness changes
   {
      R = R / 100;                                                               //  normalize 0.5 ... 2.0
      G = G / 100;
      B = B / 100;

      cim_warp_image_Vpano(imS,0);                                               //  refresh cimPXMw from cimPXMs

      ww = cimPXMw[imS]->ww;
      hh = cimPXMw[imS]->hh;

      for (py = 0; py < hh; py++)                                                //  loop all image pixels
      for (px = 0; px < ww; px++)
      {
         pixel = PXMpix(cimPXMw[imS],px,py);
         R1 = R * pixel[0];                                                      //  apply color factors
         G1 = G * pixel[1];
         B1 = B * pixel[2];

         if (R1 > 255.9 || G1 > 255.9 || B1 > 255.9) {
            bright = R1;                                                         //  avoid overflow
            if (G1 > bright) bright = G1;
            if (B1 > bright) bright = B1;
            bright = 255.9 / bright;
            R1 = R1 * bright;
            G1 = G1 * bright;
            B1 = B1 * bright;
         }

         pixel[0] = R1;
         pixel[1] = G1;
         pixel[2] = B1;
      }

      cimBlend = 1;
      zdialog_stuff(zd,"blend",cimBlend);
      cim_show_Vimages(0,0);                                                     //  combine and show with 50/50 blend
   }

   if (strmatch(event,"auto"))                                                   //  auto match color of selected image
   {
      for (im1 = imS; 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_vpano(cimPXMw[im1],1);                                //  image im1 << profile 1
         cim_adjust_colors_vpano(cimPXMw[im2],2);                                //  image im2 << profile 2
         for (imx = im1-1; imx >= imS; imx--)
            cim_adjust_colors_vpano(cimPXMw[imx],1);
      }

      for (im1 = imS-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_vpano(cimPXMw[im1],1);                                //  image im1 << profile 1
         cim_adjust_colors_vpano(cimPXMw[im2],2);                                //  image im2 << profile 2
         for (imx = im2+1; imx < cimNF; imx++)
            cim_adjust_colors_vpano(cimPXMw[imx],2);
      }

      cimBlend = 1;
      zdialog_stuff(zd,"blend",cimBlend);
      cim_show_Vimages(0,0);
   }

   if (strmatch(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);
         PXM_free(cimPXMwdup[imx]);
         cimPXMwdup[imx] = PXM_copy(cimPXMw[imx]);
         panowarp_dowarps(imx);                                                  //  re-apply mouse warps
      }

      cimBlend = 1;
      zdialog_stuff(zd,"blend",cimBlend);
      cim_show_Vimages(0,0);
   }

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

   if (strmatch(event,"mousewarp")) {                                            //  engage mouse warp/align
      zdialog_fetch(zd,"mousewarp",pano_mousewarp);
      if (pano_mousewarp)
         takeMouse(panowarp_mousefunc,dragcursor);                               //  connect mouse function
      else freeMouse();
   }

   return 1;
}


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

//  make a multi-row panorama via Panorama Tools package (Hugin version)

void m_pano_PT(GtkWidget *, cchar *)                                             //  15.02
{
   int         im, nf, err;
   char        **flist = 0, file1[100], *file2, *pp;
   FILE        *fid;
   struct stat statb;

   F1_help_topic = "panorama";                                                   //  help topic

   if (! PTtools) {
      zmessageACK(Mwin,ZTX("pano tools (hugin) not installed"));
      return;
   }

   flist = gallery_getfiles();                                                   //  get input images
   if (! flist) goto cleanup;

   for (nf = 0; flist[nf]; nf++);                                                //  count selected files
   if (nf < 2) {
      zmessageACK(Mwin,ZTX("Select at least 2 files"));
      goto cleanup;
   }

   if (checkpend("all")) return;
   Fblock = 1;                                                                   //  15.11

   err = chdir(tempdir);                                                         //  use /tmp/fotoxx-xxxxx
   if (err) {
      zmessageACK(Mwin,strerror(errno));
      goto cleanup;
   }

   fid = fopen("PTscript","w");                                                  //  build pano tools script
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      goto cleanup;
   }

   fprintf(fid,"%s","pto_gen -o PTpano.pto ");                                   //  create a project file
   for (im = 0; im < nf; im++)                                                   //    for member image files
      fprintf(fid," \"%s\" ",flist[im]);
   fprintf(fid,"\n");

   fprintf(fid,"%s\n","cpfind --celeste --multirow -o PTpano.pto PTpano.pto");      //  find control points
   fprintf(fid,"%s\n","cpclean -o PTpano.pto PTpano.pto");                          //  remove flakey ones
   fprintf(fid,"%s\n","autooptimiser -a -m -l -s -o PTpano.pto PTpano.pto");        //  optimize colors etc.
   fprintf(fid,"%s\n","pano_modify --crop=AUTO -o PTpano.pto PTpano.pto");          //  cut off black margins

   if (PTtools == 2)                                                                //  2015 version                    15.12
      fprintf(fid,"%s\n","hugin_executor --prefix=PTpano --stitching PTpano.pto");
   else {
      fprintf(fid,"%s\n","pto2mk -o PTpano.mk -p PTpano PTpano.pto");               //  convert to make file
      fprintf(fid,"%s\n","make -f PTpano.mk all");                                  //  execute make file
   }

   err = fclose(fid);
   if (err) {
      zmessageACK(Mwin,strerror(errno));
      goto cleanup;
   }

   chmod("PTscript",0760);                                                       //  run the script
   popup_command("./PTscript",600,400,Mwin,0);

   snprintf(file1,100,"%s/PTpano.tif",tempdir);                                  //  look for output file
   err = stat(file1,&statb);
   if (err) goto cleanup;

   file2 = zstrdup(flist[0],12);                                                 //  make fotoxx output file
   pp = strrchr(file2,'.');                                                      //    = ( 1st input file )-PT.tif
   if (! pp) pp = file2;
   strcpy(pp,"-PT.tif");

   err = shell_ack("cp -f \"%s\" \"%s\" ",file1,file2);                          //  copy /tmp/.../PTpano.tif
   if (err) goto cleanup;                                                        //    to fotoxx output file

   f_open(file2,0,0,1,0);                                                        //  and open it as curr. file
   
   write_popup_text("close 3");                                                  //  close script window                15.12

cleanup:

   err = chdir(get_zuserdir());                                                  //  .../.fotoxx                        16.03

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

   Fblock = 0;                                                                   //  15.11
   return;
}



