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

   Fotoxx      edit photos and manage collections

   Copyright 2007-2020 Michael Cornelison
   source code URL: https://kornelix.net
   contact: mkornelix@gmail.com

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version. See https://www.gnu.org/licenses

   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.

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

   Fotoxx image edit functions - Combine menu functions

   m_HDR                   combine and adjust images with different exposures
   m_HDF                   combine and adjust images with different focus depth
   m_stack_paint           combine multiple photos to eliminate transient objects
   m_stack_noise           combine multiple photos to reduce noise
   m_stack_layer           combine multiple image versions, select areas by mouse
   m_stack_slider          split two images with movable vertical separator
   m_image_diffs           show differences between 2 images
   m_pano_horz             combine overlapped images horizontally
   m_pano_vert             combine overlapped images vertically
   m_pano_PT               combine overlapped images using Panorama Tools (Hugin)

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

#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
int      cimLF;                                                   //  index of alphabetically last input file
char     *cimFile[10];                                            //  input image files
PXM      *cimPXMf[10];                                            //  original image pixmaps
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

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;                                          //  alignment manual warping instead of auto
int      cim_manualalign;                                         //  alignment is manual instead of auto
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
int      cim_sigdiff(float d1, float d2, float signf);            //  test 2 values for significant difference          20.0
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_trim_margins();                                      //  cut-off excess margins (panorama)
void     cim_dump_offsets(cchar *label);                          //  diagnostic tool
void     cim_flatten_image(float F);                              //  flatten panorama image
void     cim_flatten_Vimage(float F);                             //  flatten vert. panorama image


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

   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      HDR_stat;                                                               //  1 = OK, 0 = failed or canceled
float    HDR_initAlignSize = 160;                                                //  initial align image size
float    HDR_imageIncrease = 1.6;                                                //  image size increase per align cycle
float    HDR_sampSize = 10000;                                                   //  pixel sample size

float    HDR_initSearchRange = 8.0;                                              //  initial search range, +/- pixels
float    HDR_initSearchStep = 1.0;                                               //  initial search step, pixels
float    HDR_searchRange = 3.0;                                                  //  normal search range, +/- pixels
float    HDR_searchStep = 1.0;                                                   //  normal search step, pixels

float    HDR_initWarpRange = 5.0;                                                //  initial corner warp range, +/- pixels
float    HDR_initWarpStep = 1.0;                                                 //  initial corner warp step, pixels
float    HDR_warpRange = 3.0;                                                    //  normal corner warp range, +/- pixels
float    HDR_warpStep = 1.0;                                                     //  normal corner warp step, pixels

float    *HDR_bright = 0;                                                        //  maps brightness per pixel
zdialog  *HDR_adjustzd = 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        *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 (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   if (checkpend("all")) return;

   HDR_bright = 0;
   cim_manualwarp = 0;
   cimNF = 0;

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

   gallery_select_clear();                                                       //  clear selected file list
   gallery_select();                                                             //  get new list
   if (GScount == 0) goto cleanup;
   
   if (GScount < 2 || GScount > 9) {
      zmessageACK(Mwin,E2X("Select 2 to 9 files"));
      goto cleanup;
   }

   cimNF = GScount;                                                              //  file count

   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = zstrdup(GSfiles[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,E2X("Images are not all the same size"));
         goto cleanup;
      }
   }

   free_resources();                                                             //  ready to commit

   err = f_open(cimFile[cimLF]);                                                 //  curr_file = alphabetically last
   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;
      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 (HDR_stat != 1) goto cancel;

   if (Fescape) {                                                                //  user cancel                        19.0
      Fescape = 0;
      zmessage_post_bold(Mwin,"parent",3,Bcancel);
      goto cancel;
   }

   HDR_brightness();                                                             //  compute pixel brightness levels
   if (HDR_stat != 1) goto cancel;

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

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

cancel:
   edit_cancel(0);

cleanup:

   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 (HDR_bright) zfree(HDR_bright);
   *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

   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 = HDR_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 = HDR_initSearchRange;                                      //  initial align search range
      cimSearchStep = HDR_initSearchStep;                                        //  initial align search step
      cimWarpRange = HDR_initWarpRange;                                          //  initial align corner warp range
      cimWarpStep = HDR_initWarpStep;                                            //  initial align corner warp step
      cimSampSize = HDR_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);                                                    //  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
         if (Fescape) goto finish;                                               //  user kill                          19.0

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

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

         R = HDR_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 = HDR_searchRange;                                       //  align search range
         cimSearchStep = HDR_searchStep;                                         //  align search step size
         cimWarpRange = HDR_warpRange;                                           //  align corner warp range
         cimWarpStep = HDR_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;
   }

finish:
   Ffuncbusy = 0;
   HDR_stat = 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;

   HDR_bright = (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
         kk = py3 * ww + px3;
         HDR_bright[kk] = 0;
         continue;
      }

      bright = (red + green + blue) / (3 * cimNF);                               //  mean pixel brightness, 0.0 to 1.0
      kk = py3 * ww + px3;
      HDR_bright[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++)
      HDR_bright[ii] = (HDR_bright[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);
   
/***
       _____________________________________________________
      |         Adjust Image Contributions                  |
      |  _________________________________________________  |
      | |                                                 | |
      | |                                                 | |
      | |                                                 | |
      | |          curve drawing area                     | |
      | |                                                 | |
      | |                                                 | |
      | |                                                 | |
      | |_________________________________________________| |
      |  dark pixels                          light pixels  |
      |  file: xxxxxx.jpg                                   |
      |                                                     |
      |  Curve File: [Open] [Save]                          |
      |                                     [Done] [Cancel] |
      |_____________________________________________________|

***/

   HDR_adjustzd = zdialog_new(E2X("Adjust Image Contributions"),Mwin,Bdone,Bcancel,null);
   zdialog *zd = HDR_adjustzd;
   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",E2X("dark pixels"),"space=3");
   zdialog_add_widget(zd,"label","lab12","hb1",0,"expand");
   zdialog_add_widget(zd,"label","lab13","hb1",E2X("light pixels"),"space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labf1","hb2",E2X("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.sd = 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,300,250);
   zdialog_run(zd,HDR_adjust_dialog_event,"save");                               //  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.sd;    

   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
   {
      wrapup_thread(8);
      if (zd->zstat == 1) HDR_stat = 1;
      else HDR_stat = 0;
      if (HDR_stat == 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(HDR_adjustzd,"labf2",pp+1);
   signal_thread();
   return;
}


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

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

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

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      do_wthreads(HDR_adjust_wthread,NWT);                                       //  worker threads

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed
}


void * HDR_adjust_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 = HDR_bright[ii];                                                   //  mean brightness, 0.0 to 1.0
      ii = 1000 * bright;
      if (ii < 0) ii = 0;
      if (ii > 999) ii = 999;

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

   pthread_exit(0);
}


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

   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      HDF_stat;                                                               //  1 = OK, 0 = failed or canceled
float    HDF_initAlignSize = 160;                                                //  initial align image size
float    HDF_imageIncrease = 1.6;                                                //  image size increase per align cycle
float    HDF_sampSize = 10000;                                                   //  pixel sample size

float    HDF_initSearchRange = 8.0;                                              //  initial search range, +/- pixels
float    HDF_initSearchStep = 2.0;                                               //  initial search step, pixels
float    HDF_searchRange = 4.0;                                                  //  normal search range
float    HDF_searchStep = 1.0;                                                   //  normal search step

float    HDF_initWarpRange = 6.0;                                                //  initial corner warp range
float    HDF_initWarpStep = 2.0;                                                 //  initial corner warp step
float    HDF_warpRange = 3.0;                                                    //  normal corner warp range
float    HDF_warpStep = 1.0;                                                     //  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 *)
{
   int         imx, err, ww, hh;
   float       diffw, diffh;

   F1_help_topic = "HDF";                                                        //  help topic

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   if (checkpend("all")) return;

   cim_manualwarp = 0;
   cimNF = 0;

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

   gallery_select_clear();                                                       //  clear selected file list
   gallery_select();                                                             //  get new list
   if (GScount == 0) goto cleanup;
   
   if (GScount < 2 || GScount > 9) {
      zmessageACK(Mwin,E2X("Select 2 to 9 files"));
      goto cleanup;
   }

   cimNF = GScount;

   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = zstrdup(GSfiles[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,E2X("Images are not all the same size"));
         goto cleanup;
      }
   }

   free_resources();                                                             //  ready to commit

   err = f_open(cimFile[cimLF]);                                                 //  curr_file = alphabetically last
   if (err) goto cleanup;

   EFhdf.menufunc = m_HDF;
   EFhdf.funcname = "HDF";
   EFhdf.mousefunc = HDF_mousefunc;
   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 (HDF_stat != 1) goto cancel;

   if (Fescape) {                                                                //  user cancel                        19.0
      Fescape = 0;
      zmessage_post_bold(Mwin,"parent",3,Bcancel);
      goto cancel;
   }

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

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

cancel:
   edit_cancel(0);

cleanup:

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

   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 = HDF_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 = HDF_initSearchRange;                                      //  initial align search range
      cimSearchStep = HDF_initSearchStep;                                        //  initial align search step
      cimWarpRange = HDF_initWarpRange;                                          //  initial align corner warp range
      cimWarpStep = HDF_initWarpStep;                                            //  initial align corner warp step
      cimSampSize = HDF_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
         if (Fescape) goto finish;                                               //  user kill                          19.0

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

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

         R = HDF_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 = HDF_searchRange;                                       //  align search range
         cimSearchStep = HDF_searchStep;                                         //  align search step size
         cimWarpRange = HDF_warpRange;                                           //  align corner warp range
         cimWarpStep = HDF_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));

finish:
   Ffuncbusy = 0;
   HDF_stat = 1;
   thread_exit();
   return 0;                                                                     //  not executed
}


//  paint and warp output image

zdialog  *HDF_adjustzd = 0;                                                      //  paint/warp dialog
int      HDF_mode;                                                               //  mode: paint or warp
int      HDF_image;                                                              //  current image (0 based)
int      HDF_radius;                                                             //  paint mode radius
char     *HDF_pixmap = 0;                                                        //  map input image per output pixel
float    *HDF_warpx[10], *HDF_warpy[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);

/***
       ______________________________________
      |         Paint and Warp Image         |
      |                                      |
      |  Image   (o) 1  (o) 2  (o) 3  ...    |
      |  (o) paint    Radius [____]          |
      |  (o) warp                            |
      |                      [Done] [Cancel] |
      |______________________________________|

***/

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

   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",E2X("paint"));
   zdialog_add_widget(zd,"radio","warp","vbpw1",E2X("warp"));
   zdialog_add_widget(zd,"hbox","hbp","vbpw2");
   zdialog_add_widget(zd,"label","labpr","hbp",Bradius,"space=5");
   zdialog_add_widget(zd,"zspin","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

   HDF_mode = 0;                                                                 //  start in paint mode
   HDF_image = 0;                                                                //  initial image
   HDF_radius = 100;                                                             //  paint radius

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

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

   for (imx = 0; imx < cimNF; imx++) {                                           //  allocate warp memory
      ww = cimPXMw[imx]->ww;
      hh = cimPXMw[imx]->hh;
      HDF_warpx[imx] = (float *) zmalloc(ww * hh * sizeof(float));
      HDF_warpy[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,"save");                               //  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 (zd->zstat)                                                                //  dialog finish
   {
      freeMouse();                                                               //  disconnect mouse function
      signal_thread();
      wrapup_thread(8);
      if (zd->zstat == 1) HDF_stat = 1;
      else HDF_stat = 0;
      if (HDF_stat == 1) cim_trim();                                             //  cut-off edges
      HDF_mode = 0;
      zfree(HDF_pixmap);                                                         //  free pixel map
      for (imx = 0; imx < cimNF; imx++) {
         zfree(HDF_warpx[imx]);                                                  //  free warp memory
         zfree(HDF_warpy[imx]);
      }
      return 1;                                                                  //  19.0
   }

   if (strmatch(event,"paint")) {                                                //  set paint mode
      zdialog_fetch(zd,"paint",nn);
      if (! nn) return 1;
      HDF_mode = 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;
      HDF_mode = 1;
      draw_mousecircle(0,0,0,1,0);                                               //  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)
         HDF_image = nn - 1;                                                     //  0 to cimNF-1
      signal_thread();
   }

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

   if (strmatch(event,"focus")) {                                                //  toggle mouse capture
      takeMouse(HDF_mousefunc,0);                                                //  connect mouse function
      if (HDF_mode == 1)
         gdk_window_set_cursor(gdkwin,dragcursor);                               //  warp mode, drag cursor
      signal_thread();
   }

   return 1;
}


//  HDF dialog mouse function
//  paint:  during drag, selected image >> HDF_pixmap (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 (HDF_mode == 0) goto paint;
   else if (HDF_mode == 1) goto warp;
   else return;

paint:

   radius = HDF_radius;                                                          //  paintbrush radius
   radius2 = radius * radius;

   draw_mousecircle(Mxposn,Myposn,radius,0,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
      HDF_pixmap[imx] = HDF_image;

      imx = HDF_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
   }

   rect = 2 * radius;
   Fpaint3(mx-radius,my-radius,rect,rect,0);                                     //  update window
   draw_mousecircle(mx,my,radius,0,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 = HDF_pixmap[imx];                                                        //    select corresp. image to warp
   if (imx == cimNF) return;                                                     //  else no action

   if (imx != HDF_image) {
      HDF_image = imx;                                                           //  update selected image and
      imageN[5] = '1' + imx;                                                     //    dialog radio button
      zdialog_stuff(HDF_adjustzd,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;
      HDF_warpx[imx][ii] += dispx;                                               //  add this drag to prior sum
      HDF_warpy[imx][ii] += dispy;

      dispx = HDF_warpx[imx][ii];
      dispy = HDF_warpy[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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      do_wthreads(HDF_combine_wthread,NWT);                                      //  worker threads

      paintlock(0);                                                              //  unblock window paint               19.0
      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
   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 = HDF_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;
      }
   }

   pthread_exit(0);
}


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

   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 = 8.0;                                              //  initial search range, +/- pixels
float    STP_initSearchStep = 2.0;                                               //  initial search step, pixels
float    STP_searchRange = 4.0;                                                  //  normal search range
float    STP_searchStep = 1.0;                                                   //  normal search step

float    STP_initWarpRange = 6.0;                                                //  initial corner warp range
float    STP_initWarpStep = 2.0;                                                 //  initial corner warp step
float    STP_warpRange = 3.0;                                                    //  normal corner warp range
float    STP_warpStep = 0.5;                                                     //  normal corner warp step

void * STP_align_thread(void *);
void   STP_adjust();
void   STP_mousefunc();
void * STP_adjust_thread(void *);
void   STP_setpixel(int px, int py, int source);

editfunc    EFstp;                                                               //  edit function data


//  menu function

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

   F1_help_topic = "stack/paint";                                                //  help topic

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   if (checkpend("all")) return;

   cim_manualwarp = 0;
   cimNF = 0;

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

   gallery_select_clear();                                                       //  clear selected file list
   gallery_select();                                                             //  get new list
   if (GScount == 0) goto cleanup;
   
   if (GScount < 2 || GScount > 9) {
      zmessageACK(Mwin,E2X("Select 2 to 9 files"));
      goto cleanup;
   }

   cimNF = GScount;                                                              //  file count

   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = zstrdup(GSfiles[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,E2X("Images are not all the same size"));
         goto cleanup;
      }
   }

   free_resources();                                                             //  ready to commit

   err = f_open(cimFile[cimLF]);                                                 //  curr_file = alphabetically last
   if (err) goto cleanup;

   EFstp.menufunc = m_stack_paint;
   EFstp.funcname = "stack_paint";
   EFstp.mousefunc = STP_mousefunc;
   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;
   
   if (Fescape) {                                                                //  user cancel                        19.0
      Fescape = 0;
      zmessage_post_bold(Mwin,"parent",3,Bcancel);
      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:

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

   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 = 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
         if (Fescape) goto finish;                                               //  user kill                          19.0

         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
      }

      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));                                        //  image 0, no offsets

finish:
   Ffuncbusy = 0;
   STP_stat = 1;
   thread_exit();
   return 0;                                                                     //  not executed
}


//  dialog to selectively paint output image

int      STP_image;                                                              //  current image (0 based)
int      STP_radius;                                                             //  paint mode radius
int      STP_mode;                                                               //  1/2 = show/hide
float    STP_show_adjust;                                                        //  contrast adjustment
float    STP_hide_adjust;                                                        //  contrast adjustment


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

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

/***
       _________________________________________
      |        Select and Paint Image           |
      |                                         |
      | image   (o) 1  (o) 2  (o) 3  ...        |
      | paint radius [___]                      |
      |                                         |
      | transient objects                       |
      | show (o)  =========[]=================  |
      | hide (o)  ==============[]============  |
      |                                         |
      |                         [Done] [Cancel] |
      |_________________________________________|
      
***/

   zd = zdialog_new(E2X("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",Bpaintradius,"space=5");
   zdialog_add_widget(zd,"zspin","radius","hbmr","1|400|1|100");

   zdialog_add_widget(zd,"hbox","hbtrob","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labtob","hbtrob",E2X("Transient Objects"),"space=5");
   zdialog_add_widget(zd,"hbox","hbshow","dialog");
   zdialog_add_widget(zd,"vbox","vbshow1","hbshow",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbshow2","hbshow",0,"expand");
   zdialog_add_widget(zd,"radio","show","vbshow1",Bshow);
   zdialog_add_widget(zd,"radio","hide","vbshow1",Bhide);
   zdialog_add_widget(zd,"hscale","show-adjust","vbshow2","0.0|1.0|0.001|0.9","expand");
   zdialog_add_widget(zd,"hscale","hide-adjust","vbshow2","0.0|1.0|0.001|0.9","expand");

   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

   zdialog_stuff(zd,"show",1);                                                   //  initial mode, show
   zdialog_stuff(zd,"hide",0);
   STP_mode = 1;

   STP_show_adjust = 0.9;                                                        //  contrast adjustments
   STP_hide_adjust = 0.9;

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

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

   zdialog_resize(zd,250,0);
   zdialog_run(zd,STP_adjust_dialog_event,"save");                               //  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 (zd->zstat)                                                                //  dialog finish
   {
      freeMouse();
      wrapup_thread(8);
      if (zd->zstat == 1) STP_stat = 1;
      else STP_stat = 0;
      if (STP_stat == 1) cim_trim();                                             //  cut-off edges
      return 1;                                                                  //  19.0
   }

   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
   }

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(STP_mousefunc,0);                                                //  connect mouse function
   
   if (strmatch(event,"radius"))                                                 //  change paint radius
      zdialog_fetch(zd,"radius",STP_radius);

   if (strmatch(event,"show-adjust")) {                                          //  contrast adjustment
      zdialog_fetch(zd,"show-adjust",STP_show_adjust);
      signal_thread();
   }

   if (strmatch(event,"hide-adjust")) {                                          //  contrast adjustment
      zdialog_fetch(zd,"hide-adjust",STP_hide_adjust);
      signal_thread();
   }
   
   if (zstrstr("show hide",event)) {                                             //  set show/hide mode
      zdialog_fetch(zd,"show",nn);
      if (nn) STP_mode = 1;
      zdialog_fetch(zd,"hide",nn);
      if (nn) STP_mode = 2;
      signal_thread();
   }

   return 1;
}


//  STP dialog mouse function

void STP_mousefunc()
{
   int         radius, rect, radius2;
   int         mx, my, dx, dy, px3, py3;

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

   rect = 2 * radius;                                                            //  draw mouse selection circle
   draw_mousecircle(Mxposn,Myposn,radius,0,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 (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;

      if (Mbutton == 1)                                                          //  left button
         STP_setpixel(px3,py3,STP_image);                                        //  paint chosen input image

      if (Mbutton == 3)                                                          //  right button, set background
         STP_setpixel(px3,py3,-3);
   }

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

   return;
}


//  show all transient objects on top of background

void * STP_adjust_thread(void *)
{
   void * STP_adjust_wthread(void *);

   STP_setpixel(0,0,-1);                                                         //  initialization
   
   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      do_wthreads(STP_adjust_wthread,NWT);                                       //  worker threads

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();
   }

   return 0;
}


void * STP_adjust_wthread(void *arg)                                             //  worker thread
{
   int      index = *((int *) arg);
   int      px3, py3; 

   for (py3 = index+1; py3 < E3pxm->hh-1; py3 += NWT)                            //  step through output pixels
   for (px3 = 1; px3 < E3pxm->ww-1; px3++)
   {
      if (STP_mode == 0) STP_setpixel(px3,py3,-2);                               //  blend, all images equal
      if (STP_mode == 1) STP_setpixel(px3,py3,-4);                               //  show, foreground + background
      if (STP_mode == 2) STP_setpixel(px3,py3,-3);                               //  hide, background only
   }

   pthread_exit(0);
}


//  Set E3 image pixel for given pixel location and input source
//  source:  -1               initialize: pre-calculate trig functions
//           -2               mix of all input images (blend)
//           -3               background only (median pixels)
//           -4               foreground (outlier pixels) + background
//            0 - cimNF-1     specific input image (paint)

void STP_setpixel(int px3, int py3, int source)
{
   int         imx, vstat;
   int         ii, ns, ns1, ns2;
   int         Rlist[10], Glist[10], Blist[10];
   float       px, py, xoff, yoff;
   float       *pix3, pix1[4], vpix[4];
   float       R, G, B, R2, G2, B2;
   float       match, worstmatch;

   //  sorted colors [N] --> low-hi range for central group
   //  for 9 colors [xxxxxxxxx], the central group is ---xxx---  or range 3-5 from 0-8
   //                     0      1      2      3      4      5      6      7      8      9     10
   int   nsx[11][2] = { {0,0}, {0,0}, {0,0}, {1,1}, {1,2}, {2,2}, {2,3}, {3,3}, {3,4}, {4,4}, {4,5} };

   static float   sintf[10], costf[10];
   
   pix3 = PXMpix(E3pxm,px3,py3);                                                 //  output pixel

   if (source >= 0 && source < cimNF)                                            //  specific input image
   {
      imx = source;
      xoff = cimOffs[imx].xf;
      yoff = cimOffs[imx].yf;
      px = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);                //  input virtual pixel
      py = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);
      vstat = vpixel(cimPXMw[imx],px,py,vpix);
      if (vstat) memcpy(pix3,vpix,pixcc);
      return;
   }

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

   if (source == -2)                                                             //  blend all input images
   {
      R = G = B = 0;
      ns = 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);
         vstat = vpixel(cimPXMw[imx],px,py,vpix);
         if (vstat) {
            R += vpix[0];
            G += vpix[1];
            B += vpix[2];
            ns++;
         }
      }

      pix3[0] = R / ns;
      pix3[1] = G / ns;
      pix3[2] = B / ns;
      
      return;
   }

   if (source == -3)                                                             //  background only
   {
      if (cimNF < 3) return;

      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 < 3) return;

      HeapSort(Rlist,ns);                                                        //  sort RGB values
      HeapSort(Glist,ns);
      HeapSort(Blist,ns);

      R = G = B = 0;

      ns1 = nsx[ns][0];                                                          //  middle group of pixels
      ns2 = nsx[ns][1];

      for (ii = ns1; ii <= ns2; ii++)
      {
         R += Rlist[ii];
         G += Glist[ii];
         B += Blist[ii];
      }

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

      R = R / ns;                                                                //  middle group average
      G = G / ns;
      B = B / ns;

      R2 = G2 = B2 = 0;
      ns2 = 0;

      for (ii = 0; ii < ns; ii++)                                                //  get pixels matching middle group
      {
         match = RGBMATCH(R,G,B,Rlist[ii],Glist[ii],Blist[ii]);
         if (match > STP_hide_adjust) {                                          //  match threshold
            R2 += Rlist[ii];
            G2 += Glist[ii];
            B2 += Blist[ii];
            ns2++;
         }
      }
      
      pix3[0] = (R * ns + R2) / (ns + ns2);                                      //  mean background pixel
      pix3[1] = (G * ns + G2) / (ns + ns2);
      pix3[2] = (B * ns + B2) / (ns + ns2);

      return;
   }

   if (source == -4)                                                             //  show foreground pixel
   {
      STP_setpixel(px3,py3,-3);                                                  //  set pix3 to background

      worstmatch = 1.0;

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

         match = PIXMATCH(vpix,pix3);                                            //  find worst match to background
         if (match < worstmatch) {
            worstmatch = match;
            memcpy(pix1,vpix,pixcc);
         }
      }
      
      if (worstmatch < STP_show_adjust) 
         memcpy(pix3,pix1,pixcc);

      return;
   }
}


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

   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 = 10000;                                                   //  pixel sample size

float    STN_initSearchRange = 5.0;                                              //  initial search range, +/- pixels
float    STN_initSearchStep = 1.0;                                               //  initial search step, pixels
float    STN_searchRange = 2.0;                                                  //  normal search range
float    STN_searchStep = 1.0;                                                   //  normal search step

float    STN_initWarpRange = 2.0;                                                //  initial corner warp range
float    STN_initWarpStep = 1.0;                                                 //  initial corner warp 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_stack_noise(GtkWidget *, cchar *)
{
   int         imx, err, ww, hh;
   float       diffw, diffh;

   F1_help_topic = "stack/noise";                                                //  help topic

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   if (checkpend("all")) return;

   cim_manualwarp = 0;
   cimNF = 0;

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

   gallery_select_clear();                                                       //  clear selected file list
   gallery_select();                                                             //  get new list
   if (GScount == 0) goto cleanup;
   
   if (GScount < 2 || GScount > 9) {
      zmessageACK(Mwin,E2X("Select 2 to 9 files"));
      goto cleanup;
   }

   cimNF = GScount;                                                              //  file count

   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = zstrdup(GSfiles[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,E2X("Images are not all the same size"));
         goto cleanup;
      }
   }

   free_resources();                                                             //  ready to commit

   err = f_open(cimFile[cimLF]);                                                 //  curr_file = alphabetically last
   if (err) goto cleanup;

   EFstn.menufunc = m_stack_noise;
   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;

   if (Fescape) {                                                                //  user cancel                        19.0
      Fescape = 0;
      zmessage_post_bold(Mwin,"parent",3,Bcancel);
      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:

   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;

   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
         if (Fescape) goto finish;                                               //  user kill                          19.0

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

finish:
   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 low pixel  [x] omit high pixel |
      |                                         |
      |                         [Done] [Cancel] |
      |_________________________________________|
      
***/

   zd = zdialog_new(E2X("Adjust Pixel Composition"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","average","hb1",E2X("use average"),"space=3");
   zdialog_add_widget(zd,"radio","median","hb1",E2X("use median"),"space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","exlow","hb2",E2X("omit low pixel"),"space=3");
   zdialog_add_widget(zd,"check","exhigh","hb2",E2X("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,"save");                               //  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 (zd->zstat) {                                                              //  dialog finish
      if (zd->zstat == 1) STN_stat = 1;
      else STN_stat = 0;
      wrapup_thread(8);
      if (STN_stat == 1) cim_trim();                                             //  trim edges
      return 1;                                                                  //  19.0
   }

   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_adjust_wthread(void *arg);                                         //  worker thread

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      do_wthreads(STN_adjust_wthread,NWT);                                       //  worker threads

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed
}


//  worker thread

void * STN_adjust_wthread(void *arg)
{
   int         index = *((int *) arg);
   int         imx, vstat, px3, py3;
   float       red, green, blue;                                                 //  float
   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];

   //  sorted colors [N] --> low-hi range for median group
   //                     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}, {3,3}, {3,4}, {4,4}, {4,5} };

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

   pthread_exit(0);
}


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

   Stack/Layer function
   Stack images of exactly the same size with no automatic alignment.
   Choose which input image to use for each output area by mouse painting.

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

void   STL_paint();
void   STL_mousefunc();
void   STL_setpixel(int px, int py, float opacity);

int      STL_mode;
int      STL_image;                                                              //  current image (0 based)
int      STL_radius;                                                             //  mouse paint radius
int      STL_center, STL_edge;                                                   //  mouse center and edge opacity
int      STL_stat;

editfunc    EFstl;                                                               //  edit function data


//  menu function

void m_stack_layer(GtkWidget *, cchar *)                                         //  19.0
{
   int         imx, err, ww, hh;
   float       diffw, diffh;

   F1_help_topic = "stack/layer";                                                //  help topic

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   if (checkpend("all")) return;

   cim_manualwarp = 0;
   cimNF = 0;

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

   gallery_select_clear();                                                       //  clear selected file list
   gallery_select();                                                             //  get new list
   if (GScount == 0) goto cleanup;
   
   if (GScount < 2 || GScount > 9) {
      zmessageACK(Mwin,E2X("Select 2 to 9 files"));
      goto cleanup;
   }

   cimNF = GScount;                                                              //  file count

   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = zstrdup(GSfiles[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 || diffh > 0) {
         zmessageACK(Mwin,E2X("Images must be exactly the same size"));
         goto cleanup;
      }
   }

   free_resources();                                                             //  ready to commit

   err = f_open(cimFile[cimLF]);                                                 //  curr_file = alphabetically last
   if (err) goto cleanup;

   EFstl.menufunc = m_stack_layer;
   EFstl.funcname = "stack_layer";
   EFstl.mousefunc = STL_mousefunc;
   if (! edit_setup(EFstl)) goto cleanup;                                        //  setup edit (will lock)

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

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

cancel:
   edit_cancel(0);

cleanup:

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

   return;
}


//  dialog to paint output image areas with selected input image

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

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

/***
       ______________________________________
      |      Select and Paint Image          |
      |                                      |
      | Image   (o) 1  (o) 2  (o) 3  ...     |
      |                                      |
      | [ fill ] using selected image        |
      | Paint Radius  [___]                  |
      | opacity center [___]  edge [___]     |
      |                                      |
      |                      [Done] [Cancel] |
      |______________________________________|
      
***/

   zd = zdialog_new(E2X("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");

   for (imx = 0; imx < cimNF; imx++) {                                           //  radio button for each image
      imageN[5] = '1' + imx;
      labN[0] = '1' + imx;
      zdialog_add_widget(zd,"radio",imageN,"hbim",labN);
   }
   
   zdialog_add_widget(zd,"hbox","hbfill","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","fill","hbfill",E2X(" Fill "),"space=3");
   zdialog_add_widget(zd,"label","labuse","hbfill",E2X("using selected image"),"space=3");

   zdialog_add_widget(zd,"hbox","hbradius","dialog");
   zdialog_add_widget(zd,"label","labradius","hbradius",Bpaintradius,"space=3");
   zdialog_add_widget(zd,"zspin","radius","hbradius","1|500|1|100");
   
   zdialog_add_widget(zd,"hbox","hbopc","dialog");
   zdialog_add_widget(zd,"label","labcen","hbopc",Bopacitycenter,"space=3");
   zdialog_add_widget(zd,"zspin","center","hbopc","0|100|1|20","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbopc",0,"space=10");
   zdialog_add_widget(zd,"label","labedge","hbopc",Bedge,"space=3");
   zdialog_add_widget(zd,"zspin","edge","hbopc","0|100|1|0","space=3|size=3");

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

   STL_image = 0;                                                                //  initial image
   STL_mode = 1;                                                                 //  initially blend all input images
   STL_radius = 100;                                                             //  paint radius
   STL_center = 20;
   STL_edge = 0;

   for (py = 0; py < E3pxm->hh; py++)                                            //  initialize output image
   for (px = 0; px < E3pxm->ww; px++)
      STL_setpixel(px,py,1.0);
   
   Fpaint2();

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

   zdialog_resize(zd,250,0);
   zdialog_run(zd,STL_paint_dialog_event,"save");                                //  run dialog, parallel
   zdialog_wait(zd);                                                             //  wait for completion
   zdialog_free(zd);
   return;
}


//  dialog event and completion callback function

int STL_paint_dialog_event(zdialog *zd, cchar *event)
{
   int      nn, px, py;

   if (zd->zstat)                                                                //  dialog finish
   {
      freeMouse();
      if (zd->zstat == 1) STL_stat = 1;                                          //  user OK or cancel
      else STL_stat = 0;
      return 1;                                                                  //  19.0
   }

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(STL_mousefunc,0);                                                //  connect mouse function

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

   if (strmatch(event,"radius"))                                                 //  mouse paint radius
      zdialog_fetch(zd,"radius",STL_radius);

   if (strmatch(event,"center"))                                                 //  center opacity
      zdialog_fetch(zd,"center",STL_center);

   if (strmatch(event,"edge"))                                                   //  edge opactiy
      zdialog_fetch(zd,"edge",STL_edge);
   
   if (strmatch(event,"fill"))                                                   //  fill all with selected image
   {
      STL_mode = 2;
      
      for (py = 0; py < E3pxm->hh; py++)
      for (px = 0; px < E3pxm->ww; px++)
         STL_setpixel(px,py,1.0);
      
      Fpaint2();
   }

   return 1;
}


//  STL dialog mouse function

void STL_mousefunc()
{
   int      rect;
   int      mx, my, dx, dy, px, py;
   float    radius, pixrad;
   float    opacity, maxopacity, slope;

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

   radius = STL_radius;                                                          //  paintbrush radius

   draw_mousecircle(Mxposn,Myposn,radius,0,0);

   if (! Mxdrag && ! Mydrag) return;                                             //  motion event
   
   maxopacity = 1.0 - radius / 550.0;                                            //  1.0 - 0.09 for radius 0 - 500
   maxopacity = 0.1;
   slope = 0.01 * (STL_edge - STL_center) / radius;

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

   STL_mode = 2;

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

      px = mx + dx;                                                              //  output pixel
      py = my + dy;
      if (px < 0 || px > E3pxm->ww-1) continue;                                  //  outside image
      if (py < 0 || py > E3pxm->hh-1) continue;

      opacity = maxopacity * (0.01 * STL_center + slope * pixrad);               //  reduce opacity for large radius
      STL_setpixel(px, py, opacity);                                             //  paint chosen input image
   }

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

   draw_mousecircle(mx,my,radius,0,0);                                           //  draw mouse selection circle

   return;
}


//  Set E3 image pixel for given pixel location and input source
//
//       STL_mode:   1 = mix of all input images (blend)
//                   2 = specific input image (paint)
//       STL_image:  0 - cimNF-1 = specific input image

void STL_setpixel(int px, int py, float opacity)
{
   int         imx;
   float       *pix3, *pix1;
   float       R, G, B;

   pix3 = PXMpix(E3pxm,px,py);                                                   //  output pixel

   if (STL_mode == 1)                                                            //  blend all input images
   {
      R = G = B = 0;

      for (imx = 0; imx < cimNF; imx++)
      {
         pix1 = PXMpix(cimPXMf[imx],px,py);
         R += pix1[0];
         G += pix1[1];
         B += pix1[2];
      }

      pix3[0] = R / cimNF;
      pix3[1] = G / cimNF;
      pix3[2] = B / cimNF;
   }
   
   if (STL_mode == 2)                                                            //  specific input image
   {
      imx = STL_image;
      pix1 = PXMpix(cimPXMf[imx],px,py);
      pix3[0] = pix1[0] * opacity + pix3[0] * (1.0 - opacity);
      pix3[1] = pix1[1] * opacity + pix3[1] * (1.0 - opacity);
      pix3[2] = pix1[2] * opacity + pix3[2] * (1.0 - opacity);
   }

   return;
}


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

   Stack/slider function
   Stack images of exactly the same size with no automatic alignment.
   The two images are split left and right by a movable vertical boundary.

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

void   STS_start();
void   STS_mousefunc();
void   STS_slider();
int    STS_stat;
int    Eww, Ehh, P, PP;
int    Finit;

editfunc    EFsts;                                                               //  edit function data


//  menu function

void m_stack_slider(GtkWidget *, cchar *)                                        //  20.0
{
   int         imx, err;
   int         diffw, diffh;

   F1_help_topic = "stack/slider";                                               //  help topic

   if (FGWM != 'F' && FGWM != 'G') return;
   if (checkpend("all")) return;

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

   gallery_select_clear();                                                       //  clear selected file list
   gallery_select();                                                             //  get new list
   if (GScount == 0) goto cleanup;
   
   if (GScount != 2) {
      zmessageACK(Mwin,E2X("Select 2 files"));
      goto cleanup;
   }

   cimNF = GScount;                                                              //  file count

   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = zstrdup(GSfiles[imx]);                                      //  set up file list

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

   Eww = cimPXMf[0]->ww;
   Ehh = cimPXMf[0]->hh;

   for (imx = 1; imx < cimNF; imx++)                                             //  check image compatibility
   {
      diffw = Eww - cimPXMf[imx]->ww;
      diffh = Ehh - cimPXMf[imx]->hh;
      if (diffw != 0 || diffh != 0) {
         zmessageACK(Mwin,E2X("Images must be exactly the same size"));
         goto cleanup;
      }
   }
   
   free_resources();                                                             //  ready to commit

   err = f_open(cimFile[0]);                                                     //  curr_file = 1st chosen
   if (err) goto cleanup;

   EFsts.menufunc = m_stack_slider;
   EFsts.funcname = "stack_slider";
   EFsts.mousefunc = STS_mousefunc;
   if (! edit_setup(EFsts)) goto cleanup;                                        //  setup edit (will lock)
   
   PXM_addalpha(E3pxm);

   STS_start();                                                                  //  combine images based on mouse position
   if (STS_stat != 1) goto cancel;

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

cancel:
   edit_cancel(0);

cleanup:

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

   return;
}


//  dialog to paint output image areas with selected input image

void STS_start()
{
   int STS_start_dialog_event(zdialog *zd, cchar *event);

   zdialog  *zd;

/***
       _____________________________
      |     Split two Images        |
      |                             |
      |    drag image boundary      |
      |                             |
      |             [Done] [Cancel] |
      |_____________________________|
      
***/

   zd = zdialog_new(E2X("Split two Images"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"label","labtip","dialog",E2X("drag image boundary"));

   P = PP = Eww / 2;                                                             //  initial 50/50 split
   Finit = 1;
   STS_slider();

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

   zdialog_run(zd,STS_start_dialog_event,"save");                                //  run dialog, parallel
   zdialog_wait(zd);                                                             //  wait for completion
   zdialog_free(zd);
   return;
}


//  dialog event and completion callback function

int STS_start_dialog_event(zdialog *zd, cchar *event)
{
   if (zd->zstat)                                                                //  dialog finish
   {
      freeMouse();
      if (zd->zstat == 1) STS_stat = 1;                                          //  user OK or cancel
      else STS_stat = 0;
      return 1;
   }

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(STS_mousefunc,0);                                                //  connect mouse function

   return 1;
}


//  STS dialog mouse function

void STS_mousefunc()
{
   if (! Mxdrag && ! Mydrag) return;                                             //  wait for drag event
   
   P = Mxdrag;                                                                   //  mouse drag x position, 0...Eww-1
   STS_slider();

   Mxdrag = Mydrag = 0;
   return;
}


//  output revised E3 image
//  use input image 0 for pixels left of P and image 1 for pixels right of P

void STS_slider()
{
   float       *pix3, *pix1;
   int         px, py, rx, cc;
   int         pcc = E3pxm->nc * sizeof(float);
   float       red[4] = { 255, 0, 0, 255 };
   int         Rww;                                                              //  1/2 red line width
   static int  PRww;                                                             //  prior Rww

   Rww = 3 / Mscale;   
   if (Rww < 1) Rww = 1;
   
   if (Finit)                                                                    //  initial 50/50 image
   {
      Finit = 0;
      PRww = Rww;

      for (py = 0; py < Ehh; py++)
      {
         px = 0;                                                                 //  left image
         cc = P-Rww;
         if (cc > 0) {
            pix1 = PXMpix(cimPXMf[0],px,py); 
            pix3 = PXMpix(E3pxm,px,py);
            memcpy(pix3,pix1, cc * pcc);
         }

         for (px = P-Rww; px <= P+Rww; px++)                                     //  red line
         {      
            if (px >= 0 && px < Eww) {
               pix3 = PXMpix(E3pxm,px,py);
               memcpy(pix3,red,pcc);
            }
         }

         px = P+Rww;                                                             //  right image
         cc = Eww - px - 1;
         if (cc > 0) {
            pix1 = PXMpix(cimPXMf[1],px,py); 
            pix3 = PXMpix(E3pxm,px,py);
            memcpy(pix3,pix1, cc * pcc);
         }
      }

      Fpaint2();
   }

   if (P == PP) return;                                                          //  no movement

   px = cc = 0;

   for (py = 0; py < Ehh; py++)
   {
      if (P > PP)                                                                //  movement to right
      {
         px = PP - 2 * PRww;
         cc = P - PP + 4 * PRww;
         if (px < 0) px = 0;
         if (px + cc > Eww-1) cc = Eww-1 - px;
         pix1 = PXMpix(cimPXMf[0],px,py);                                        //  expose left image
         pix3 = PXMpix(E3pxm,px,py);
         memcpy(pix3,pix1, cc * pcc);
      }

      if (P < PP)                                                                //  movement to left
      {
         px = P - 2 * PRww;
         cc = PP - P + 4 * PRww;
         if (px < 0) px = 0;
         if (px + cc > Eww-1) cc = Eww-1 - px;
         pix1 = PXMpix(cimPXMf[1],px,py);                                        //  expose right image
         pix3 = PXMpix(E3pxm,px,py);
         memcpy(pix3,pix1, cc * pcc);
      }

      for (rx = P - Rww; rx <= P + Rww; rx++)                                    //  red line
      {      
         if (rx >= 0 && rx < Eww) {
            pix3 = PXMpix(E3pxm,rx,py);
            memcpy(pix3,red,pcc);
         }
      }
   }
   
   PP = P;                                                                       //  save prior position
   PRww = Rww;                                                                   //  save prior 1/2 red line width
   
   if (px < 0) px = 0;
   if (px + cc > Eww-1) cc = Eww-1 - px;
   
   Fpaint3(px,0,cc,Ehh,0);                                                       //  paint new exposed area
   return;
}


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

//  show the differences between two images

namespace imagediffs_names
{
   char     *imagefile1, *imagefile2;
   PXB      *pxb1, *pxb2, *pxbdiffs;
   int      xalign, yalign;
}


//  menu function

void m_image_diffs(GtkWidget *, const char *menu)
{
   using namespace imagediffs_names;

   int    imagediffs_dialog_event(zdialog* zd, const char *event);

   F1_help_topic = "image diffs";
   
   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

   imagefile1 = imagefile2 = 0;
   pxb1 = pxb2 = pxbdiffs = 0;

/***
             ________________________________
            |        Image Differences       |
            |                                |
            |  Select 2 files [select]       |
            |   (o) imagename1.jpg           |
            |   (o) imagename2.jpg           |
            |   (o) differences              |
            |                                |
            |  X-align [___]  Y-align [___]  |
            |                                |
            |                [done] [cancel] |
            |________________________________|
            
***/   

   zdialog *zd = zdialog_new(E2X("Image Differences"),Mwin,Bdone,Bcancel,null);
   
   zdialog_add_widget(zd,"hbox","hbsel","dialog");
   zdialog_add_widget(zd,"label","labsel","hbsel",E2X("Select 2 files"),"space=3");
   zdialog_add_widget(zd,"button","select","hbsel",Bselect,"space=3");
   zdialog_add_widget(zd,"hbox","hbshow","dialog");
   zdialog_add_widget(zd,"vbox","vbshow1","hbshow",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vbshow2","hbshow",0,"space=5|homog");
   zdialog_add_widget(zd,"hbox","hbshow21","vbshow2");
   zdialog_add_widget(zd,"hbox","hbshow22","vbshow2");
   zdialog_add_widget(zd,"hbox","hbshow23","vbshow2");
   zdialog_add_widget(zd,"radio","image1","vbshow1",0);
   zdialog_add_widget(zd,"radio","image2","vbshow1",0);
   zdialog_add_widget(zd,"radio","diffs","vbshow1",0);
   zdialog_add_widget(zd,"label","labimage1","hbshow21",Bnoselection);
   zdialog_add_widget(zd,"label","labimage2","hbshow22",Bnoselection);
   zdialog_add_widget(zd,"label","labdiffs","hbshow23",E2X("differences"));
   zdialog_add_widget(zd,"hbox","hbalign","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labxalign","hbalign",E2X("X-align"),"space=3");
   zdialog_add_widget(zd,"zspin","xalign","hbalign","-999|999|1|0","space=3");
   zdialog_add_widget(zd,"label","space","hbalign",0,"space=5");
   zdialog_add_widget(zd,"label","labyalign","hbalign",E2X("Y-align"),"space=3");
   zdialog_add_widget(zd,"zspin","yalign","hbalign","-999|999|1|0","space=3");

   zdialog_run(zd,imagediffs_dialog_event,"save");                               //  run dialog - parallel
   return;
}


//  imagediffs dialog event and completion function

int imagediffs_dialog_event(zdialog *zd, const char *event)
{
   using namespace imagediffs_names;
   
   void  imagediffs_thread();

   int      err;
   char     *pp;
   GError   *gerror = 0;
   char     *outfile;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   
   if (zd->zstat)                                                                //  done or cancel
   {
      Fpxb = 0;
      if (zd->zstat == 1) {                                                      //  done
         if (pxbdiffs) {
            pp = file_new_version(imagefile1);
            outfile = zgetfile(Bsave,MWIN,"save",pp,0);                          //  save differences to PNG file
            zfree(pp);
            if (outfile) {
               pp = strrchr(outfile,'.');
               if (pp && strlen(pp) >= 4) strcpy(pp,".jpg");
               gdk_pixbuf_save(pxbdiffs->pixbuf,outfile,"jpeg",&gerror,null);
               if (gerror) zmessageACK(Mwin,gerror->message);
               else {
                  f_open(outfile,0,0,1,0);                                       //  open diffs file
                  update_image_index(outfile);
               }
               zfree(outfile);
            }
         }
      }
      else {                                                                     //  cancel or [x]                      20.0
         if (imagefile1) f_open(imagefile1);                                     //  fall back to previous file
         else if (curr_file) f_open(curr_file);
      }
      
      if (imagefile1) zfree(imagefile1);                                         //  free resources
      if (imagefile2) zfree(imagefile2);
      if (pxb1) PXB_free(pxb1);
      if (pxb2) PXB_free(pxb2);
      if (pxbdiffs) PXB_free(pxbdiffs);

      zdialog_free(zd);
      return 1;
   }

   if (strmatch(event,"select"))                                                 //  select 2 input files
   {
      gallery_select_clear();
      err = gallery_select();                                                    //  choose thumbnails
      if (err) return 1;
      if (GScount != 2) {
         zmessageACK(Mwin,E2X("select exactly 2 files"));
         return 1;
      }

      if (imagefile1) zfree(imagefile1);                                         //  get chosen files
      imagefile1 = zstrdup(GSfiles[0]);
      if (imagefile2) zfree(imagefile2);
      imagefile2 = zstrdup(GSfiles[1]);
      
      f_open(imagefile1);                                                        //  open the first file                20.0

      pp = strrchr(imagefile1,'/');                                              //  stuff dialog with chosen file names
      if (! pp) pp = imagefile1;
      else pp++;
      zdialog_stuff(zd,"labimage1",pp);

      pp = strrchr(imagefile2,'/');
      if (! pp) pp = imagefile2;
      else pp++;
      zdialog_stuff(zd,"labimage2",pp);

      if (pxb1) PXB_free(pxb1);                                                  //  free prior PXBs
      if (pxb2) PXB_free(pxb2);
      if (pxbdiffs) PXB_free(pxbdiffs);
      pxb1 = pxb2 = pxbdiffs = 0;
      
      pxb1 = PXB_load(imagefile1,1);                                             //  make PXB images from files
      if (! pxb1) return 1;
      pxb2 = PXB_load(imagefile2,1);
      if (! pxb2) return 1;

      Fpxb = pxb1;                                                               //  show image1
      m_viewmode(0,"F");
      Fpaint2();

      xalign = yalign = 0;                                                       //  reset alignment
      return 1;
   }

   if (strmatch(event,"image1")) {
      if (pxb1) Fpxb = pxb1;
      Fpaint2();
   }
   
   if (strmatch(event,"image2")) {
      if (pxb2) Fpxb = pxb2;
      Fpaint2();
   }
   
   if (strmatch(event,"diffs")) {
      imagediffs_thread();
      if (pxbdiffs) Fpxb = pxbdiffs;
      Fpaint2();
   }
   
   if (strmatch(event,"xalign")) {
      zdialog_fetch(zd,"xalign",xalign);
      imagediffs_thread();
      zdialog_stuff(zd,"diffs",1);
   }
   
   if (strmatch(event,"yalign")) {
      zdialog_fetch(zd,"yalign",yalign);
      imagediffs_thread();
      zdialog_stuff(zd,"diffs",1);
   }

   return 1;
}


//  thread function - multiple working threads to update image

void imagediffs_thread()
{
   using namespace imagediffs_names;

   void  * imagediffs_wthread(void *arg);                                        //  worker thread
   
   if (! pxb1 || ! pxb2) return;
   
   while (Fpaintrequest) zmainsleep(0.01);
   paintlock(1);                                                                 //  block window paint                 19.0

   if (pxbdiffs) PXB_free(pxbdiffs);
   pxbdiffs = PXB_make(pxb1->ww,pxb1->hh,3);

   do_wthreads(imagediffs_wthread,NWT);                                          //  worker threads

   Fpxb = pxbdiffs;

   paintlock(0);                                                                 //  unblock window paint               19.0
   Fpaint2();

   return;
}


void * imagediffs_wthread(void *arg)                                             //  worker thread function
{
   using namespace imagediffs_names;

   int      index = *((int *) arg);
   int      px1, py1, px2, py2;
   int      xlo, xhi, ylo, yhi;
   uint8    *pix1, *pix2, *pix3;
   uint8    red, green, blue;
   
   if (xalign > 0) {
      xlo = xalign;
      xhi = pxb1->ww;
   }
   else {
      xlo = 0;
      xhi = pxb1->ww + xalign;
   }
   
   if (yalign > 0) {
      ylo = yalign;
      yhi = pxb1->hh;
   }
   else {
      ylo = 0;
      yhi = pxb1->hh + yalign;
   }
   
   if (xhi > pxb2->ww) xhi = pxb2->ww;
   if (yhi > pxb2->hh) yhi = pxb2->hh;
   
   for (py1 = ylo + index; py1 < yhi; py1 += NWT)
   for (px1 = xlo; px1 < xhi; px1++)
   {
      px2 = px1 - xalign;
      py2 = py1 - yalign;
      
      pix1 = pxb1->pixels + py1 * pxb1->rs + px1 * 3;
      pix2 = pxb2->pixels + py2 * pxb2->rs + px2 * 3;
      pix3 = pxbdiffs->pixels + py1 * pxbdiffs->rs + px1 * 3;
      
      red = pix1[0] - pix2[0];
      green = pix1[1] - pix2[1];
      blue = pix1[2] - pix2[2];

      if (red < 0) red = -red;
      if (green < 0) green = -green;
      if (blue < 0) blue = -blue;
      
      pix3[0] = red;
      pix3[1] = green;
      pix3[2] = blue;
   }

   pthread_exit(0);                                                              //  exit thread
}


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

    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 auto-align blend width
float panFinalBlend = 0.10;                                                      //  final blend width * ww 
float panBlendDecrease = 0.7;                                                    //  BW reduction per align cycle

float panInitSearchRange = 15.0;                                                 //  initial search range, +/- pixels
float panInitSearchStep = 1.0;                                                   //  initial search step, pixels
float panSearchRange = 4.0;                                                      //  normal search range, +/- pixels
float panSearchStep = 1.0;                                                       //  normal search step, pixels

float panInitWarpRange = 15.0;                                                   //  initial corner warp range, +/- pixels
float panInitWarpStep = 1.0;                                                     //  initial corner warp step, pixels
float panWarpRange = 8.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
PXM   *preflatPXM;                                                               //  copy of E3pxm before flatten

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_horz(GtkWidget *, cchar *)
{
   int      imx, err;

   F1_help_topic = "panorama";                                                   //  help topic

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   if (checkpend("all")) return;

   cim_manualwarp = 0;
   pano_mousewarp = 0;
   cim_manualalign = 0;
   cimNF = 0;

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

   gallery_select_clear();                                                       //  clear selected file list
   gallery_select();                                                             //  get new list
   if (GScount == 0) goto cleanup;
   
   if (GScount < 2 || GScount > 4) {
      zmessageACK(Mwin,E2X("Select 2 to 4 files"));
      goto cleanup;
   }

   cimNF = GScount;                                                              //  file count

   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = zstrdup(GSfiles[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[cimLF]);                                                 //  curr_file = alphabetically last
   if (err) goto cleanup;

   EFpano.menufunc = m_pano_horz;
   EFpano.funcname = "pano";
   EFpano.mousefunc = panowarp_mousefunc;
   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);
   if (Fescape) {                                                                //  user cancel                        19.0
      Fescape = 0;
      zmessage_post_bold(Mwin,"parent",3,Bcancel);
   }

cleanup:

   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[2] = { exif_focal_length_35_key, exif_focal_length_key };
   char        *pp[2] = { 0, 0 };
   cchar       *lens_source;
   float       temp;

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

   err = 1;
   lens_source = "NO EXIF";
   exif_get(curr_file,exifkey,pp,2);                                             //  get lens mm from EXIF if available
   if (pp[0]) err = convSF(pp[0], temp, 20, 1000);                               //  try both keys                      19.16
   else if (pp[1]) err = convSF(pp[1], 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;
   }

   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                        |
      | [Resize] resize window                  |
      | [Search] Search for lens mm             |
      | [Save] Save lens mm -> image EXIF       |
      |                                         |
      |                     [Proceed] [Cancel]  |
      |_________________________________________|

*/

   panozd = zdialog_new(E2X("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,"zspin","spmm","hb1","20|999|0.1|35","space=5");
   zdialog_add_widget(panozd,"label","labmm","hb1",E2X("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",E2X("no auto warp"),"space=5");
   zdialog_add_widget(panozd,"hbox","hbma","dialog");
   zdialog_add_widget(panozd,"check","manalign","hbma",E2X("manual align"),"space=5");
   zdialog_add_widget(panozd,"hbox","hb5","dialog",0,"space=2");
   zdialog_add_widget(panozd,"button","resize","hb5",E2X("Resize"),"space=5");
   zdialog_add_widget(panozd,"label","labsiz","hb5",E2X("resize window"),"space=5");
   zdialog_add_widget(panozd,"hbox","hb6","dialog",0,"space=2");
   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",0,"space=2");
   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",E2X("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,"save");                               //  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
   return;
}


//  pre-align dialog event function

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

   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
      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,E2X("use two images only"));
      else  panStat = 2;                                                         //  tell thread to search
      return 1;
   }

   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
      
      m_zoom(0,"fit");                                                           //  reset poss. user zoom-in           20.0

      if (! panStat) return 1;                                                   //  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,E2X("Too little overlap, cannot align"));
            panStat = 0;
            return 1;
         }
      }
   }

   return 1;
}


//  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,E2X("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 (cim_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;

   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

   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,E2X("Too little overlap, cannot align"));
            goto fail;
         }
         
         if (cim_manualalign) cim_show_images(0,0);
         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;
            if (Fescape) goto fail;
         }

         dx = cimOffs[im2].xf - offsets0.xf;                                     //  changes from initial offsets
         dy = cimOffs[im2].yf - offsets0.yf;
         dt = cimOffs[im2].tf - offsets0.tf;

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

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

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

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

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

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

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

success:
   panStat = 1;
   goto align_done;
fail:
   panStat = 0;
align_done:
   cimBlend = 1;                                                                 //  tiny blend (increase in adjust)
   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 = E2X("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          |
      |   flatten image [___] [apply]     |
      |                                   |
      |                  [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",E2X("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,"zspin","red","hbc2","50|200|0.1|100");
   zdialog_add_widget(panozd,"zspin","green","hbc2","50|200|0.1|100");
   zdialog_add_widget(panozd,"zspin","blue","hbc2","50|200|0.1|100");
   zdialog_add_widget(panozd,"hbox","hbbri","dialog",0,"space=3");
   zdialog_add_widget(panozd,"label","labbr","hbbri",Bbrightness,"space=5");
   zdialog_add_widget(panozd,"zspin","bright","hbbri","50|200|0.1|100");
   zdialog_add_widget(panozd,"button","RGBapply","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",E2X("auto color"),"space=5");
   zdialog_add_widget(panozd,"button","file","hbc3",E2X("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,"zspin","blend","hbblen","1|999|1|1");
   zdialog_add_widget(panozd,"button","blendapply","hbblen",Bapply,"space=15");
   zdialog_add_widget(panozd,"hbox","hbwarp","dialog",0,"space=3");
   zdialog_add_widget(panozd,"check","mousewarp","hbwarp",E2X("mouse warp"),"space=3");
   zdialog_add_widget(panozd,"hbox","hbflat","dialog",0,"space=3");
   zdialog_add_widget(panozd,"label","labflat","hbflat",E2X("flatten image"),"space=3");
   zdialog_add_widget(panozd,"zspin","flatten","hbflat","0|1|0.01|0","space=3");
   zdialog_add_widget(panozd,"button","flatapply","hbflat",Bapply,"space=15");

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

   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,"save");                                 //  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, F;
   int         imS, imx, im1, im2;
   int         ww, hh, px, py, nn;
   float       bright, bright2, *pixel;

   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]);
      if (preflatPXM) PXM_free(preflatPXM);
      preflatPXM = 0;
      return 1;
   }

   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 (zstrstr("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,"RGBapply"))                                               //  apply color & brightness changes
   {
      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

      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
   {
      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

      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 (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

      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,"blendapply"))                                             //  apply new blend width
   {
      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

      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
   {
      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

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

      zdialog_fetch(zd,"mousewarp",pano_mousewarp);
      if (pano_mousewarp)
         takeMouse(panowarp_mousefunc,dragcursor);                               //  connect mouse function
      else freeMouse();
   }

   if (strmatch(event,"flatapply"))
   {
      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = PXM_copy(preflatPXM);
      }
      else preflatPXM = PXM_copy(E3pxm);

      zdialog_fetch(zd,"flatten",F);
      cim_flatten_image(F);

      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_pano_vert(GtkWidget *, cchar *)
{
   int      imx, err;

   F1_help_topic = "vert. panorama";                                          //  help topic

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   if (checkpend("all")) return;

   cim_manualwarp = 0;
   pano_mousewarp = 0;
   cim_manualalign = 0;
   cimNF = 0;

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

   gallery_select_clear();                                                       //  clear selected file list
   gallery_select();                                                             //  get new list
   if (GScount == 0) goto cleanup;
   
   if (GScount < 2 || GScount > 4) {
      zmessageACK(Mwin,E2X("Select 2 to 4 files"));
      goto cleanup;
   }

   cimNF = GScount;                                                              //  file count

   for (imx = 0; imx < cimNF; imx++)
      cimFile[imx] = zstrdup(GSfiles[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[cimLF]);                                                 //  curr_file = alphabetically last
   if (err) goto cleanup;

   EFvpano.menufunc = m_pano_vert;
   EFvpano.funcname = "vpano";
   EFvpano.mousefunc = panowarp_mousefunc;
   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);
   if (Fescape) {                                                                //  user cancel                        19.0
      Fescape = 0;
      zmessage_post_bold(Mwin,"parent",3,Bcancel);
   }

cleanup:

   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[2] = { exif_focal_length_35_key, exif_focal_length_key };
   char        *pp[2] = { 0, 0 };
   cchar       *lens_source;
   float       temp;

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

   err = 1;
   lens_source = "NO EXIF";
   exif_get(curr_file,exifkey,pp,2);                                             //  get lens mm from EXIF if available
   if (pp[0]) err = convSF(pp[0], temp, 20, 1000);                               //  try both keys                      19.16
   else if (pp[1]) err = convSF(pp[1], 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;
   }

   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                        |
      | [Resize] resize window                  |
      | [Search] Search for lens mm             |
      | [Save] Save lens mm -> image EXIF       |
      |                                         |
      |                     [Proceed] [Cancel]  |
      |_________________________________________|

*/

   panozd = zdialog_new(E2X("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,"zspin","spmm","hb1","20|999|0.1|35","space=5");
   zdialog_add_widget(panozd,"label","labmm","hb1",E2X("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",E2X("no auto warp"),"space=5");
   zdialog_add_widget(panozd,"hbox","hbma","dialog");
   zdialog_add_widget(panozd,"check","manalign","hbma",E2X("manual align"),"space=5");
   zdialog_add_widget(panozd,"hbox","hb5","dialog",0,"space=2");
   zdialog_add_widget(panozd,"button","resize","hb5",E2X("Resize"),"space=5");
   zdialog_add_widget(panozd,"label","labsiz","hb5",E2X("resize window"),"space=5");
   zdialog_add_widget(panozd,"hbox","hb6","dialog",0,"space=2");
   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",0,"space=2");
   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",E2X("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,"save");                              //  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
   return;
}


//  pre-align dialog event function

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

   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
      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,E2X("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

      m_zoom(0,"fit");                                                           //  reset poss. user zoom-in           20.0

      if (! panStat) return 1;                                                   //  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,E2X("Too little overlap, cannot align"));
            panStat = 0;
            return 1;
         }
      }
   }

   return 1;
}


//  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.02);                                                              //  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,E2X("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 (cim_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;

   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

   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,E2X("Too little overlap, cannot align"));
            goto fail;
         }

         if (cim_manualalign) cim_show_Vimages(0,0);
         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;
            if (Fescape) goto fail;
         }

         dx = cimOffs[im2].xf - offsets0.xf;                                     //  changes from initial offsets
         dy = cimOffs[im2].yf - offsets0.yf;
         dt = cimOffs[im2].tf - offsets0.tf;

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

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

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

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

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

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

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

success:
   panStat = 1;
   goto align_done;
fail:
   panStat = 0;
align_done:
   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 = E2X("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                  |
      |   [flatten] curved image          |
      |   flatten image [___] [apply]     |
      |                                   |
      |                  [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,"zspin","red","hbc2","50|200|0.1|100","space=5");
   zdialog_add_widget(panozd,"zspin","green","hbc2","50|200|0.1|100","space=5");
   zdialog_add_widget(panozd,"zspin","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,"zspin","bright","hbbri","50|200|0.1|100");
   zdialog_add_widget(panozd,"button","RGBapply","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",E2X("auto color"),"space=5");
   zdialog_add_widget(panozd,"button","file","hbc3",E2X("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,"zspin","blend","hbblen","1|999|1|1");
   zdialog_add_widget(panozd,"button","blendapply","hbblen",Bapply,"space=15");
   zdialog_add_widget(panozd,"hbox","hbwarp","dialog",0,"space=3");
   zdialog_add_widget(panozd,"check","mousewarp","hbwarp",E2X("mouse warp"),"space=3");
   zdialog_add_widget(panozd,"hbox","hbflat","dialog",0,"space=3");
   zdialog_add_widget(panozd,"label","labflat","hbflat",E2X("flatten image"),"space=3");
   zdialog_add_widget(panozd,"zspin","flatten","hbflat","0|1|0.01|0","space=3");
   zdialog_add_widget(panozd,"button","flatapply","hbflat",Bapply,"space=15");

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

   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,"save");                                //  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, F;
   int         imS, imx, im1, im2;
   int         ww, hh, px, py, nn;
   float       bright, bright2, *pixel;

   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]);
      if (preflatPXM) PXM_free(preflatPXM);
      preflatPXM = 0;
      return 1;
   }

   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 (zstrstr("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,"RGBapply"))                                               //  apply color & brightness changes
   {
      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

      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
   {
      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

      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 (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

      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,"blendapply"))                                             //  apply new blend width
   {
      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

      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
   {
      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

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

      zdialog_fetch(zd,"mousewarp",pano_mousewarp);
      if (pano_mousewarp)
         takeMouse(panowarp_mousefunc,dragcursor);                               //  connect mouse function
      else freeMouse();
   }

   if (strmatch(event,"flatapply"))
   {
      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = PXM_copy(preflatPXM);
      }
      else preflatPXM = PXM_copy(E3pxm);

      zdialog_fetch(zd,"flatten",F);
      cim_flatten_Vimage(F);
      Fpaint2();
   }

   return 1;
}


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

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

void m_pano_PT(GtkWidget *, cchar *)
{
   int         im, ii, err;
   char        file1[100], *file2, *pp;
   char        cfolder[XFCC];
   FILE        *fid;
   struct stat statb;

   F1_help_topic = "PT panorama";                                                //  help topic

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

   cimNF = 0;

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

   gallery_select_clear();                                                       //  clear selected file list
   gallery_select();                                                             //  get new list
   if (GScount == 0) goto cleanup;

   if (GScount < 2) {
      zmessageACK(Mwin,E2X("Select at least 2 files"));
      goto cleanup;
   }
   
   cimNF = GScount;

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

   pp = getcwd(cfolder,XFCC);                                                    //  save curr. folder

   err = chdir(temp_folder);                                                     //  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 < cimNF; im++)                                                //    for member image files
      fprintf(fid," \"%s\" ",GSfiles[im]);
   fprintf(fid,"\n");

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

   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",temp_folder);                              //  look for output file
   err = stat(file1,&statb);
   if (err) goto cleanup;

   im = 0;
   for (ii = 1; ii < cimNF; ii++)                                                //  get alphabetically last input file
      if (strcmp(GSfiles[ii],GSfiles[im]) > 0) im = ii;

   file2 = zstrdup(GSfiles[im],12);                                              //  make fotoxx output file
   pp = strrchr(file2,'.');                                                      //    = < last input file > -PT.tif
   if (! pp) pp = file2;
   strcpy(pp,"-PT.tif");

   err = copyFile(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
   m_viewmode(0,"F");
   
cleanup:

   err = chdir(cfolder);                                                         //  restore curr. folder
   Fblock = 0;
   return;
}


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

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

int cim_load_files()
{
   PXM      *pxm;
   int      imx;

   for (imx = 0; imx < cimNF; imx++)
   {
      PXM_free(cimPXMf[imx]);
      pxm = PXM_load(cimFile[imx],1);                                            //  ACK errors
      if (! pxm) return 0;
      PXM_addalpha(pxm);
      cimPXMf[imx] = pxm;
   }
   
   cimLF = 0;                                                                    //  find alphabetically last file
   for (imx = 1; imx < cimNF; imx++)
      if (strcmp(cimFile[imx],cimFile[cimLF]) > 0) cimLF = imx;

   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

      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

      ++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 (jj < 256 && ii > brlev2[rgb][jj]) jj++;                             //  find matching brlev2 brightness
      if (jj == 256) jj = 255;
      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 (jj < 256 && ii > brlev1[rgb][jj]) jj++;                             //  find matching brlev1 brightness
      if (jj == 256) jj = 255;
      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
      if (npix < 1) continue;                                                    //  bug fix
      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
         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
{
   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;
}


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

   cimPanoFL = F;                                                                //  save focal length globally
   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];

   do_wthreads(cim_warp_image_wthread,NWT);                                      //  worker threads

   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
   }

   pthread_exit(0);
}


//  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;
}


//  compare two floats for significant difference
//  signf: e.g. 0.01 for 1% threshold of significance
//  return:  0  difference not significant
//          +1  d1 > d2
//          -1  d1 < d2

int cim_sigdiff(float d1, float d2, float signf)
{
   float diff = fabsf(d1-d2);
   if (diff == 0.0) return 0;
   diff = diff / (fabsf(d1) + fabsf(d2));
   if (diff < signf) return 0;
   if (d1 > d2) return 1;
   else return -1;
}


//  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 (cim_sigdiff(match,matchB,0.00001) > 0) {
            matchB = match;                                                      //  save best match
            offsetsB = cimOffs[im2];
         }

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

         if (Fescape) goto finish;                                               //  user kill                          19.0
      }

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

      if (cim_manualwarp) continue;                                              //  skip auto warp

      //  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 (cim_sigdiff(match,matchB,0.00001) > 0) {
               matchB = match;                                                   //  save best match
               offsetsB = cimOffs[im2];
            }

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

            if (Fescape) goto finish;                                            //  user kill                          19.0
         }

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

finish:
   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

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

         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
         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
      
         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
         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 paint                 19.0

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

   do_wthreads(cim_show_images_wthread,NWT);                                     //  worker threads

   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 paint               19.0
   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       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] < 250) continue;                                            //  voided pixel

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

   pthread_exit(0);
}


//  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 paint                 19.0

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

   do_wthreads(cim_show_Vimages_wthread,NWT);                                    //  worker threads

   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 paint               19.0
   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       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

   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] < 250) continue;                                            //  voided pixel

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

   pthread_exit(0);
}


//  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;
}


//  cut off excess margins (panorama)

void cim_trim_margins()
{
   int      ww, hh;
   int      xlo, xhi, ylo, yhi;
   int      px, py;
   float    *pix, *pix1, *pix2;
   PXM      *pxmout;
   
   ww = E3pxm->ww;
   hh = E3pxm->hh;

   xlo = ww;
   xhi = 0;
   ylo = hh;
   yhi = 0;
   
   for (py = 0; py < hh; py += 2)                                                //  find extent of empty margins
   for (px = 0; px < ww; px += 2)
   {
      pix = PXMpix(E3pxm,px,py);
      if (pix[3] == 0) continue;
      if (px < xlo) xlo = px;
      if (px > xhi) xhi = px;
      if (py < ylo) ylo = py;
      if (py > yhi) yhi = py;
   }
   
   ww = xhi - xlo;                                                               //  make new image just large enough
   hh = yhi - ylo;
   pxmout = PXM_make(ww,hh,4);
   if (! pxmout) return;

   for (py = 0; py < hh; py++)                                                   //  copy pixels into new image
   for (px = 0; px < ww; px++)
   {
      pix1 = PXMpix(E3pxm,px+xlo,py+ylo);
      pix2 = PXMpix(pxmout,px,py);
      memcpy(pix2,pix1,pixcc);
   }

   PXM_free(E3pxm);                                                              //  replace input with output PXM
   E3pxm = pxmout;
   
   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;
}


//  uncurve the combined panorama image
//  input F = 0.0 - 1.0 = no unbend - full unbend

void cim_flatten_image(float F)                                                  //  scaled range
{
   PXM      *pxmout;
   int      ww, hh, vstat;
   float    ww2, hh2;
   int      px, py, rx, ry;
   float    dx, dy;
   float    sx, sy;
   float    tx, ty;
   float    *pix, vpix[4];
   float    *pix1, *pix2;
                                                                                 //  F = 0.0   0.2   0.4   0.6   0.8   1.0
   F = 1.0 - F;                                                                  //      1.0   0.8   0.6   0.4   0.2   0.0
   F = F * 3.0;                                                                  //      3.0   2.4   1.8   1.2   0.6   0.0
   F = pow(2,F);                                                                 //      8.0   5.3   3.5   1.7   1.5   1.0
   F = F * cimPanoFL;                                                            //      8*FL (min flat) to 1*FL (max flat)

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

   for (ry = 0; ry < hh; ry++)                                                   //  copy image to middle
   for (rx = 0; rx < ww; rx++)
   {
      px = rx + 0.2 * ww;
      py = ry + 0.1 * hh;
      pix1 = PXMpix(E3pxm,rx,ry);
      pix2 = PXMpix(pxmout,px,py);
      memcpy(pix2,pix1,pixcc);
   }
   
   PXM_free(E3pxm);
   E3pxm = pxmout;                                                               //  new image with bigger margins

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

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

   for (py = 0; py < hh; py++)                                                   //  copy and flatten image
   for (px = 0; px < ww; px++)
   {
      dx = px - ww2;
      dy = py - hh2;
      tx = atanf(dx / F);
      ty = atanf(dy / F * cosf(tx));
      sx = F * tx;
      sy = F * ty;
      sy += hh2;
      sx += ww2;
      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;
   
   cim_trim_margins();                                                           //  trim excess margins
   return;
}


//  version for vertical panorama

void cim_flatten_Vimage(float F)
{
   PXM      *pxmout;
   int      ww, hh, vstat;
   float    ww2, hh2;
   int      px, py, rx, ry;
   float    dx, dy;
   float    sx, sy;
   float    tx, ty;
   float    *pix, vpix[4];
   float    *pix1, *pix2;
                                                                                 //  F = 0.0   0.2   0.4   0.6   0.8   1.0
   F = 1.0 - F;                                                                  //      1.0   0.8   0.6   0.4   0.2   0.0
   F = F * 3.0;                                                                  //      3.0   2.4   1.8   1.2   0.6   0.0
   F = pow(2,F);                                                                 //      8.0   5.3   3.5   1.7   1.5   1.0
   F = F * cimPanoFL;                                                            //      8*FL (min flat) to 1*FL (max flat)

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

   for (ry = 0; ry < hh; ry++)                                                   //  copy image to middle
   for (rx = 0; rx < ww; rx++)
   {
      px = rx + 0.1 * ww;
      py = ry + 0.2 * hh;
      pix1 = PXMpix(E3pxm,rx,ry);
      pix2 = PXMpix(pxmout,px,py);
      memcpy(pix2,pix1,pixcc);
   }
   
   PXM_free(E3pxm);
   E3pxm = pxmout;                                                               //  new image with bigger margins

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

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

   for (py = 0; py < hh; py++)                                                   //  copy and flatten image
   for (px = 0; px < ww; px++)
   {
      dy = py - hh2;
      dx = px - ww2;
      ty = atanf(dy / F);
      tx = atanf(dx / F * cosf(ty));
      sy = F * ty;
      sx = F * tx;
      sy += hh2;
      sx += ww2;
      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;
   
   cim_trim_margins();                                                           //  trim excess margins
   return;
}


