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

   Fotocx - edit photos and manage collections

   Copyright 2007-2024 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.

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

   Fotocx 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_layers          combine multiple image layers, select and paint areas
   m_stack_split           split two images with movable vertical separator
   m_panorama              combine overlapped images horizontally
   m_cim_show_files        show input files for prior combine edit function

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

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

using namespace zfuncs;

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

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

int      cimNF;                                                   //  image count, <= 10
int      cimLF;                                                   //  index of alphabetically last input file
ch       *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    cimSampSize;                                             //  alignment, pixel sample size
float    cimAlignSize;                                            //  alignment image size (max ww/hh)
float    cimImageIncrease;                                        //  increase image size per align stage
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    cimMinSearchRange;                                       //  minimum search range
float    cimSearchStep;                                           //  alignment search step, vpixels
float    cimWarpRange;                                            //  alignment corner warp range, pixels
float    cimMinWarpRange;                                         //  minimum warp range
float    cimWarpStep;                                             //  alignment corner warp step, vpixels
int      cim_manualwarp;                                          //  alignment manual warping instead of auto
int      cim_manualalign;                                         //  alignment is manual instead of auto
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[*]]
ch       *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_images()

int      cim_get_files(int min, int max);                         //  get list of input files, load PXM images
int      cim_load_files();                                        //  load input files
int      cim_check_files(float maxdiff);                          //  check input files are compatible
void     cim_free_memory();                                       //  free image memory after completion
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_get_redpix(int im1);                                 //  find high-contrast pixels in overlap area
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_align_images(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_trim();                                              //  cut-off edges where all images do not overlap
void     cim_add_meta_hist();                                     //  add list of files used to edit history
void     cim_dump_offsets(ch *label);                             //  diagnostic tool


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

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

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

int      HDR_stat;                                                               //  1 = OK, 0 = failed or canceled
float    *HDR_bright = 0;                                                        //  maps brightness per pixel
float    HDR_respfac[10][1000];                                                  //  contribution / image / pixel brightness

void     *HDR_align(void *);                                                     //  align 2 images
void     HDR_brightness();                                                       //  compute pixel brightness levels
void     HDR_adjust_dialog();                                                    //  adjust image contribution curves
void     *HDR_adjust_weights(void *);                                            //  combine images per contribution curves

editfunc    EFhdr;                                                               //  edit function data


//  menu function

void m_HDR(GtkWidget *, ch *menu)
{
   ch          *ftemp;
   int         imx, jj, err, px, py, ww, hh;
   float       cimbrite[10], btemp;
   double      pixsum, fnorm = 1.0 / 768.0;
   float       *pixel;
   PXM         *pxmtemp;
   
   F1_help_topic = "HDR";

   Plog(1,"m_HDR \n");

   if (CEF) {
      zmessageACK(Mwin,"finish or cancel pending edit");                         //  24.40
      return;
   }
   
   if (Fblock("HDR")) return;                                                    //  check for blocking function           24.40
   Fblock(0);

   cimNF = cim_get_files(2,10);                                                  //  get 2-10 files
   if (! cimNF) return;

   if (! cim_load_files()) return;                                               //  load and check all files
   
   if (! cim_check_files(0.02)) return;                                          //  check files are compatible

   free_resources();                                                             //  ready to commit
   
   HDR_bright = 0;
   cim_manualwarp = 0;

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

   EFhdr.menufunc = m_HDR;
   EFhdr.menuname = "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]);
      }
      cimbrite[imx] = pixsum / (ww * hh);                                        //  image mean brightness, 0.0 - 1.0
   }
   
   for (imx = 0; imx < cimNF; imx++)                                             //  sort file and pixmap lists
   for (jj = imx+1; jj < cimNF; jj++)                                            //    by decreasing brightness
   {
      if (cimbrite[jj] > cimbrite[imx]) {                                        //  bubble sort
         btemp = cimbrite[jj];
         cimbrite[jj] = cimbrite[imx];
         cimbrite[imx] = btemp;
         ftemp = cimFile[jj];
         cimFile[jj] = cimFile[imx];
         cimFile[imx] = ftemp;
         pxmtemp = cimPXMf[jj];
         cimPXMf[jj] = cimPXMf[imx];
         cimPXMf[imx] = pxmtemp;
      }
   }

   Fescape = 0;

   HDR_align(0);
   if (HDR_stat != 1) goto cancel;

   if (Fescape)  {                                                               //  user cancel
      zmessage_post(Mwin,"parent",3,"function canceled");
      goto cancel;
   }

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

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

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

cancel:
   edit_cancel(0);

cleanup:
   cim_free_memory();                                                            //  free memory for input files
   if (HDR_bright) zfree(HDR_bright);
   *paneltext = 0;
   Fescape = 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(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

   Funcbusy(+1);

   cimPano = 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;

      cimAlignSize = 200;                                                        //  initial alignment image size 
      cimImageIncrease = 1.6;                                                    //  size increase per align stage
      cimSampSize = 5000;                                                        //  alignment pixel sample size
      cimSearchRange = 16;                                                       //  initial alignment search range
      cimMinSearchRange = 4;                                                     //  minimum search range
      cimSearchStep = 1;                                                         //  alignment search step
      cimWarpRange = 16;                                                         //  initial alignment warp range
      cimMinWarpRange = 4;                                                       //  minimum warp range
      cimWarpStep = 1;                                                           //  alignment warp step

      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 = cimAlignSize / 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

      cimNsearch = 0;                                                            //  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_images(im1,im2);                                              //  align im2 to im1
         if (Fescape)  goto finish;                                              //  user kill

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

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

         R = cimImageIncrease;                                                   //  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 = cimSearchRange / cimImageIncrease + 0.5;
         if (cimSearchRange < cimMinSearchRange) cimSearchRange = cimMinSearchRange;
         cimWarpRange = cimWarpRange / cimImageIncrease + 0.5;
         if (cimWarpRange < cimMinWarpRange) cimWarpRange = cimMinWarpRange;
      }

      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:
   Funcbusy(-1);
   HDR_stat = 1;
   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;

   Funcbusy(+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),"cim HDR");              //  get memory for brightness array

   minbright = 1.0;
   maxbright = 0.0;

   for (py3 = 0; py3 < hh; py3++)                                                //  step through all output pixels
   for (px3 = 0; px3 < ww; px3++)
   {
      red = green = blue = 0;
      vstat = 0;

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

         px = costf[imx] * (px3 - xoff) + sintf[imx] * (py3 - yoff);             //  image N pixel, after offsets
         py = costf[imx] * (py3 - yoff) - sintf[imx] * (px3 - xoff);
         vstat = vpixel(cimPXMw[imx],px,py,vpix);
         if (! vstat) break;

         red += fnorm * vpix[0];                                                 //  sum input pixels
         green += fnorm * vpix[1];
         blue += fnorm * vpix[2];
      }

      if (! vstat) {                                                             //  pixel outside some image
         pix3 = PXMpix(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.0 / cimNF;
      pix3[1] = green * 255.0 / cimNF;
      pix3[2] = blue * 255.0 / 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;

   Funcbusy(-1);
   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_dialog()
{
   int    HDR_adjust_dialog_event(zdialog *zd, ch *event);
   void   HDR_curvedit(int);

   int         imx;
   float       cww = 1.0 / (cimNF-1);
   
/***
       _____________________________________________________
      |         Adjust Image Contributions                  |
      |  _________________________________________________  |
      | |                                                 | |
      | |                                                 | |
      | |                                                 | |
      | |          curve drawing area                     | |
      | |                                                 | |
      | |                                                 | |
      | |                                                 | |
      | |_________________________________________________| |
      |  dark pixels                          light pixels  |
      |  press F1 for help                                  |
      |                                            [OK] [X] |
      |_____________________________________________________|

***/

   zdialog *zd = zdialog_new("Adjust Image Contributions",Mwin,"OK"," X ",null);
   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","dark pixels","space=3");
   zdialog_add_widget(zd,"label","lab12","hb1",0,"expand");
   zdialog_add_widget(zd,"label","lab13","hb1","light pixels","space=3");
   zdialog_add_widget(zd,"hbox","hbf1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labf1","hbf1","Press F1 for help","space=5");

   GtkWidget *brframe = zdialog_gtkwidget(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);
   }

   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, ch *event)                              //  add file open dialog
{
   if (zd->zstat)                                                                //  dialog complete
   {
      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)
{
   HDR_adjust_weights(0);
   return;
}


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

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

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

   for (imx = 0; imx < cimNF; imx++)                                             //  loop input images
   {
      for (ii = 0; ii < 1000; ii++)                                              //  loop all brightness levels
      {
         HDR_respfac[imx][ii] = 0;                                               //  remove range limits
         xval = 0.001 * ii;
         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;
   }

   paintlock(1);                                                                 //  block window paint
   do_wthreads(HDR_adjust_wthread,NSMP);                                         //  worker threads
   paintlock(-1);                                                                //  unblock window paint

   Fpaint2();                                                                    //  update window
   return 0;
}


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 += NSMP)                                      //  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;
   }

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

void * HDF_align(void *);
void   HDF_adjust_dialog();
void   HDF_mousefunc();
void * HDF_map_pixels(void *);

editfunc    EFhdf;                                                               //  edit function data


//  menu function

void m_HDF(GtkWidget *, ch *menu)
{
   int      err;

   F1_help_topic = "HDF";

   Plog(1,"m_HDF \n");
   
   if (CEF) {
      zmessageACK(Mwin,"finish or cancel pending edit");                         //  24.40
      return;
   }

   if (Fblock("HDF")) return;                                                    //  check for blocking function           24.40
   Fblock(0);

   cimNF = cim_get_files(2,10);                                                  //  get 2-10 files
   if (! cimNF) return;

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

   if (! cim_check_files(0.02)) return;                                          //  check files are compatible

   free_resources();                                                             //  ready to commit

   cim_manualwarp = 0;

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

   EFhdf.menufunc = m_HDF;
   EFhdf.menuname = "HDF";
   EFhdf.mousefunc = HDF_mousefunc;
   if (! edit_setup(EFhdf)) goto cleanup;                                        //  setup edit (will lock)

   Fescape = 0;

   HDF_align(0);                                                                 //  align each pair of images
   if (HDF_stat != 1) goto cancel;

   if (Fescape)  {                                                               //  user cancel
      zmessage_post(Mwin,"parent",3,"function canceled");
      goto cancel;
   }

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

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

cancel:
   edit_cancel(0);

cleanup:

   cim_free_memory();                                                            //  free memory for input files
   *paneltext = 0;
   Fescape = 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(void *)
{
   int         imx, im1, im2, ww, hh, ii, nn;
   float       R;

   Funcbusy(+1);

   cimPano = 0;                                                                  //  no pano mode

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

      cimAlignSize = 200;                                                        //  initial alignment image size
      cimImageIncrease = 1.6;                                                    //  size increase per align stage
      cimSampSize = 5000;                                                        //  alignment pixel sample size
      cimSearchRange = 16;                                                       //  initial alignment search range
      cimMinSearchRange = 4;                                                     //  minimum search range
      cimSearchStep = 1;                                                         //  alignment search step
      cimWarpRange = 16;                                                         //  initial alignment warp range
      cimMinWarpRange = 4;                                                       //  minimum warp range
      cimWarpStep = 1;                                                           //  alignment warp step

      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 = cimAlignSize / 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

      cimNsearch = 0;                                                            //  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_images(im1,im2);                                              //  align im2 to im1
         if (Fescape)  goto finish;                                              //  user kill

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

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

         R = cimImageIncrease;                                                   //  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 = cimSearchRange / cimImageIncrease + 0.5;
         if (cimSearchRange < cimMinSearchRange) cimSearchRange = cimMinSearchRange;
         cimWarpRange = cimWarpRange / cimImageIncrease + 0.5;
         if (cimWarpRange < cimMinWarpRange) cimWarpRange = cimMinWarpRange;
      }

      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:
   Funcbusy(-1);
   HDF_stat = 1;
   return 0;
}


//  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
ch       *HDF_pixmap = 0;                                                        //  map input image per output pixel
float    *HDF_warpx[10], *HDF_warpy[10];                                         //  warp memory, pixel displacements


void HDF_adjust_dialog()
{
   ch       imageN[8] = "imageN", labN[4] = "0";
   int      cc, imx, ww, hh;

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

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

***/

   HDF_adjustzd = zdialog_new("Paint and Warp Image",Mwin,"OK"," X ",null);
   zdialog *zd = HDF_adjustzd;

   zdialog_add_widget(zd,"hbox","hbim","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labim","hbim","Image","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","paint");
   zdialog_add_widget(zd,"radio","warp","vbpw1","warp");
   zdialog_add_widget(zd,"hbox","hbp","vbpw2");
   zdialog_add_widget(zd,"label","labpr","hbp","Radius","space=5");
   zdialog_add_widget(zd,"zspin","radius","hbp","1|400|1|300");
   zdialog_add_widget(zd,"label","space","vbpw2");
   zdialog_add_widget(zd,"hbox","hbf1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labf1","hbf1","Press F1 for help","space=5");

   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
   
   zdialog_add_ttip(zd,"paint","drag mouse to show selected image");             //  24.10
   zdialog_add_ttip(zd,"warp","warp and align selected image with mouse drags");

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

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

   cc = E3pxm->ww * E3pxm->hh;                                                   //  allocate pixel map
   HDF_pixmap = (ch *) zmalloc(cc,"cim HDF");
   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),"cim HDF");
      HDF_warpy[imx] = (float *) zmalloc(ww * hh * sizeof(float),"cim HDF");
   }

   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, ch *event)
{
   int      imx, nn;

   if (zd->zstat)                                                                //  dialog finish
   {
      freeMouse();                                                               //  disconnect mouse function
      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;
   }

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

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

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

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


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

void * HDF_map_pixels(void *)
{
   void * HDF_map_wthread(void *);

   do_wthreads(HDF_map_wthread,NSMP);                                            //  worker threads
   Fpaint2();
   return 0;
}


void * HDF_map_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 += NSMP)                           //  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;
      }
   }

   return 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

void * STP_align(void *);
void   STP_adjust_dialog();
void   STP_mousefunc();
void * STP_map_pixels(void *);
void   STP_setpixel(int px, int py, int source);

editfunc    EFstp;                                                               //  edit function data


//  menu function

void m_stack_paint(GtkWidget *, ch *menu)
{
   int      err;

   F1_help_topic = "stack/paint";

   Plog(1,"m_stack_paint \n");

   if (CEF) {
      zmessageACK(Mwin,"finish or cancel pending edit");                         //  24.40
      return;
   }

   if (Fblock("stack/paint")) return;                                            //  check for blocking function           24.40
   Fblock(0);

   cimNF = cim_get_files(2,10);                                                  //  get 2-10 files
   if (! cimNF) return;

   if (! cim_load_files()) return;                                               //  load and check all files
   
   if (! cim_check_files(0.02)) return;                                          //  check files are compatible

   free_resources();                                                             //  ready to commit

   cim_manualwarp = 0;

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

   EFstp.menufunc = m_stack_paint;
   EFstp.menuname = "Stack/Paint";
   EFstp.mousefunc = STP_mousefunc;
   if (! edit_setup(EFstp)) goto cleanup;                                        //  setup edit (will lock)

   Fescape = 0;

   STP_align(0);                                                                 //  align each pair of images
   if (STP_stat != 1) goto cancel;
   
   if (Fescape)  {                                                               //  user cancel
      zmessage_post(Mwin,"parent",3,"function canceled");
      goto cancel;
   }

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

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

cancel:
   edit_cancel(0);

cleanup:

   cim_free_memory();                                                            //  free memory for input files
   *paneltext = 0;
   Fescape = 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(void *)
{
   int         imx, im1, im2, ww, hh, ii, nn;
   float       R;

   Funcbusy(+1);
   cimPano = 0;                                                                  //  no pano mode

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

      cimAlignSize = 200;                                                        //  initial alignment image size
      cimImageIncrease = 1.6;                                                    //  size increase per align stage
      cimSampSize = 5000;                                                        //  alignment pixel sample size
      cimSearchRange = 16;                                                       //  initial alignment search range
      cimMinSearchRange = 4;                                                     //  minimum search range
      cimSearchStep = 1;                                                         //  alignment search step
      cimWarpRange = 16;                                                         //  initial alignment warp range
      cimMinWarpRange = 4;                                                       //  minimum warp range
      cimWarpStep = 1;                                                           //  alignment warp step

      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 = cimAlignSize / 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

      cimNsearch = 0;                                                            //  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_images(im1,im2);                                              //  align im2 to im1
         if (Fescape)  goto finish;                                              //  user kill

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

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

         R = cimImageIncrease;                                                   //  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 = cimSearchRange / cimImageIncrease + 0.5;
         if (cimSearchRange < cimMinSearchRange) cimSearchRange = cimMinSearchRange;
         cimWarpRange = cimWarpRange / cimImageIncrease + 0.5;
         if (cimWarpRange < cimMinWarpRange) cimWarpRange = cimMinWarpRange;
      }

      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:
   Funcbusy(-1);
   STP_stat = 1;
   return 0;
}


//  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_dialog()
{
   zdialog  *zd;
   ch       imageN[8] = "imageN", labN[4] = "0";
   int      imx;

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

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

   zd = zdialog_new("Select and Paint Image",Mwin,"OK"," X ",null);
   zdialog_add_widget(zd,"hbox","hbim","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labim","hbim","Image","space=5");
   zdialog_add_widget(zd,"hbox","hbmr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labr","hbmr","Paint Radius","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","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","Show");
   zdialog_add_widget(zd,"radio","hide","vbshow1","Hide");
   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");
   zdialog_add_widget(zd,"hbox","hbf1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labf1","hbf1","Press F1 for help","space=5");

   zdialog_add_ttip(zd,"labim","select image to show with mouse paint");         //  24.10
   zdialog_add_ttip(zd,"show","show transient objects");
   zdialog_add_ttip(zd,"hide","hide transient objects");

   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;
   
   STP_map_pixels(0);

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

   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, ch *event)
{
   int      nn;

   if (zd->zstat)                                                                //  dialog finish
   {
      freeMouse();
      if (zd->zstat == 1) STP_stat = 1;
      else STP_stat = 0;
      if (STP_stat == 1) cim_trim();                                             //  cut-off edges
      return 1;
   }

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

   if (strmatch(event,"hide-adjust")) {                                          //  contrast adjustment
      zdialog_fetch(zd,"hide-adjust",STP_hide_adjust);
      STP_map_pixels(0);
   }
   
   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;
      STP_map_pixels(0);
   }

   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 or suppress transient objects on top of background

void * STP_map_pixels(void *)
{
   void * STP_map_wthread(void *);

   STP_setpixel(0,0,-1);                                                         //  initialization
   do_wthreads(STP_map_wthread,NSMP);                                            //  worker threads
   Fpaint2();
   return 0;
}


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

   for (py3 = index+1; py3 < E3pxm->hh-1; py3 += NSMP)                           //  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
   }

   return 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];
   uint8       rl, gl, bl;
   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
      {
         rl = Rlist[ii];
         gl = Glist[ii];
         bl = Blist[ii];
         match = RGBmatch(R,G,B,rl,gl,bl);
         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.

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

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(void *);
void   STN_adjust_dialog();
void * STN_combine(void *);

editfunc    EFstn;                                                               //  edit function data


//  menu function

void m_stack_noise(GtkWidget *, ch *menu)
{
   int     err;

   F1_help_topic = "stack/noise";

   Plog(1,"m_stack_noise \n");

   if (CEF) {
      zmessageACK(Mwin,"finish or cancel pending edit");                         //  24.40
      return;
   }

   if (Fblock("stack/noise")) return;                                            //  check for blocking function           24.40
   Fblock(0);

   cimNF = cim_get_files(2,10);                                                  //  get 2-10 files
   if (! cimNF) return;

   if (! cim_load_files()) return;                                               //  load and check all files
   
   if (! cim_check_files(0.02)) return;                                          //  check files are compatible

   free_resources();                                                             //  ready to commit

   cim_manualwarp = 0;

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

   EFstn.menufunc = m_stack_noise;
   EFstn.menuname = "Stack/Noise";
   if (! edit_setup(EFstn)) goto cleanup;                                        //  setup edit (will lock)

   Fescape = 0;

   STN_align(0);                                                                 //  align each pair of images
   if (STN_stat != 1) goto cancel;

   if (Fescape)  {                                                               //  user cancel
      zmessage_post(Mwin,"parent",3,"function canceled");
      goto cancel;
   }

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

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

cancel:
   edit_cancel(0);

cleanup:

   cim_free_memory();                                                            //  free memory for input files
   *paneltext = 0;
   Fescape = 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(void *)
{
   int         imx, im1, im2, ww, hh, ii, nn;
   float       R;

   Funcbusy(+1);
   cimPano = 0;                                                                  //  no pano mode

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

      cimAlignSize = 200;                                                        //  initial alignment image size
      cimImageIncrease = 1.6;                                                    //  size increase per align stage
      cimSampSize = 5000;                                                        //  alignment pixel sample size
      cimSearchRange = 16;                                                       //  initial alignment search range
      cimMinSearchRange = 4;                                                     //  minimum search range
      cimSearchStep = 1;                                                         //  alignment search step
      cimWarpRange = 16;                                                         //  initial alignment warp range
      cimMinWarpRange = 4;                                                       //  minimum warp range
      cimWarpStep = 1;                                                           //  alignment warp step

      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 = cimAlignSize / 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

      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_images(im1,im2);                                              //  align im2 to im1
         if (Fescape)  goto finish;                                              //  user kill

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

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

         R = cimImageIncrease;                                                   //  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 = cimSearchRange / cimImageIncrease + 0.5;
         if (cimSearchRange < cimMinSearchRange) cimSearchRange = cimMinSearchRange;
         cimWarpRange = cimWarpRange / cimImageIncrease + 0.5;
         if (cimWarpRange < cimMinWarpRange) cimWarpRange = cimMinWarpRange;
      }

      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:
   Funcbusy(-1);
   STN_stat = 1;
   return 0;
}


//  change pixel combination according to user input

void STN_adjust_dialog()
{
   zdialog     *zd;

   int STN_adjust_dialog_event(zdialog *zd, ch *event);

/***
       _________________________________________
      |       Adjust Pixel Composition          |
      |                                         |
      | (o) use average  (o) use median         |
      | [x] omit low pixel  [x] omit high pixel |
      |                                         |
      |                                [OK] [X] |
      |_________________________________________|
      
***/

   zd = zdialog_new("Adjust Pixel Composition",Mwin,"OK"," X ",null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","average","hb1","use average","space=3");
   zdialog_add_widget(zd,"radio","median","hb1","use median","space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","exlow","hb2","omit low pixel","space=3");
   zdialog_add_widget(zd,"check","exhigh","hb2","omit high pixel","space=3");

   zdialog_stuff(zd,"average",1);                                                //  default = average
   zdialog_stuff(zd,"median",0);
   zdialog_stuff(zd,"exlow",0);
   zdialog_stuff(zd,"exhigh",0);

   STN_average = 1;
   STN_median = 0;
   STN_exlow = 0;
   STN_exhigh = 0;

   STN_combine(0);

   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, ch *event)
{
   if (zd->zstat) {                                                              //  dialog finish
      if (zd->zstat == 1) STN_stat = 1;
      else STN_stat = 0;
      if (STN_stat == 1) cim_trim();                                             //  trim edges
      return 1;
   }

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

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

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

   if (strmatch(event,"exhigh")) {
      zdialog_fetch(zd,"exhigh",STN_exhigh);
      STN_combine(0);
   }

   return 1;
}


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

void * STN_combine(void *)
{
   void * STN_combine_wthread(void *arg);                                        //  worker thread

   do_wthreads(STN_combine_wthread,NSMP);                                        //  worker threads
   Fpaint2();                                                                    //  update window
   return 0;
}


//  worker thread

void * STN_combine_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 += NSMP)                           //  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;
   }

   return 0;
}


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

   Stack/layers function
   Stack images of exactly the same size with no automatic alignment.
   Choose images and mix ratios to use for each output area by mouse painting.

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

namespace STL_names
{
   float    imageR[10];                                                          //  image blend ratios
   float    imageC[10];                                                          //  image coefficients (sum = 1) 
   int      STL_mode;                                                            //  1/2 = all/mouse area
   int      mouseRad;                                                            //  mouse radius
   int      powerCen, powerEdge;                                                 //  mouse opacity, center and edge
   int      STL_stat;
   
   editfunc    EFSTL;                                                            //  edit function data
}


//  menu function

void m_stack_layers(GtkWidget *, ch *menu)                                       //  new 24.30
{
   using namespace STL_names;

   void   STL_mousefunc();
   void   STL_paint();

   int     err;

   F1_help_topic = "stack/layers";

   Plog(1,"m_stack_layers \n");

   if (CEF) {
      zmessageACK(Mwin,"finish or cancel pending edit");                         //  24.40
      return;
   }

   if (Fblock("stack/layers")) return;                                           //  check for blocking function           24.40
   Fblock(0);

   cimNF = cim_get_files(2,10);                                                  //  get 2-10 files
   if (! cimNF) return;

   if (! cim_load_files()) return;                                               //  load and check all files
   
   if (! cim_check_files(0.0)) return;                                           //  check files are compatible

   free_resources();                                                             //  ready to commit

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

   EFSTL.menufunc = m_stack_layers;
   EFSTL.menuname = "Stack/layers";
   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;
   cim_add_meta_hist();
   edit_done(0);
   goto cleanup;

cancel:
   edit_cancel(0);

cleanup:
   cim_free_memory();                                                            //  free memory for input files
   return;
}


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

void STL_paint()
{
   using namespace STL_names;

   void   STL_paintall();
   int    STL_paint_dialog_event(zdialog *zd, ch *event);
   void   STL_mousefunc();

   zdialog  *zd;
   ch       imageN[8];
   int      imx;
   float    sum;

/***
       ______________________________________
      |      Select and Paint Image          |
      |                                      |
      | Image Ratios  [10]  [40]  [20]  ...  |                                   //  imageR[] blend ratios
      | (o) Paint entire image               |                                   //  STL_mode 1
      | (o) Paint within mouse               |                                   //  STL_mode 2
      |   Mouse Radius [___]                 |                                   //  mouseRad
      |   Power Center [__]  Edge [__]       |                                   //  powerCen, powerEdge
      |                                      |  
      |                             [OK] [X] |
      |______________________________________|
      
***/

   zd = zdialog_new("Select and Paint Image",Mwin,"OK"," X ",null);              //  image ratio spin buttons
   zdialog_add_widget(zd,"hbox","hbim","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labim","hbim","Image Ratios","space=3");

   for (imx = 0; imx < cimNF; imx++) {                                           //  button name = image0, image1 ...
      snprintf(imageN,8,"image%d",imx);
      zdialog_add_widget(zd,"zspin",imageN,"hbim","0|100|1|10","space=5");
      imageR[imx] = 10;                                                          //  initial value
   }
   
   zdialog_add_widget(zd,"radio","mode1","dialog","Paint entire image","space=5");     //  paint all radio button

   zdialog_add_widget(zd,"radio","mode2","dialog","Paint within mouse","space=1");     //  paint within mouse radio button
   
   zdialog_add_widget(zd,"hbox","hbrad","dialog",0,"space=1");                   //  mouse radius spin button
   zdialog_add_widget(zd,"label","space","hbrad",0,"space=10");
   zdialog_add_widget(zd,"label","labrad","hbrad","Mouse Radius","space=3");
   zdialog_add_widget(zd,"zspin","mouseRad","hbrad","0|500|1|100","space=3");
   
   zdialog_add_widget(zd,"hbox","hbpow","dialog",0,"space=1");                   //  power center spin button
   zdialog_add_widget(zd,"label","space","hbpow",0,"space=10");
   zdialog_add_widget(zd,"label","labcen","hbpow","Power Center","space=3");
   zdialog_add_widget(zd,"zspin","powerCen","hbpow","0|100|1|100","space=3");

   zdialog_add_widget(zd,"label","space","hbpow",0,"space=10");                  //  power edge spin button
   zdialog_add_widget(zd,"label","labedge","hbpow","Edge","space=3");
   zdialog_add_widget(zd,"zspin","powerEdge","hbpow","0|100|1|10","space=3");

   zdialog_stuff(zd,"mode1",1);                                                  //  select STL_mode 1 
   zdialog_stuff(zd,"mode2",0);

   STL_mode = 1;                                                                 //  initially blend all images
   mouseRad = 100;                                                               //  initial mouse radius
   powerCen = 100;                                                               //  initial mouse power
   powerEdge = 10;

   for (sum = imx = 0; imx < cimNF; imx++)                                       //  sum image blend factors
      sum += imageR[imx];
   if (sum < 1) sum = 1;

   for (imx = 0; imx < cimNF; imx++)                                             //  image coefficients (sum = 1)
      imageC[imx] = imageR[imx] / sum;

   STL_paintall();                                                               //  initial output image

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

   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, ch *event)
{
   using namespace STL_names;

   void  STL_mousefunc();
   void  STL_paintall();

   int      nn, imx;
   float    sum;
   
   if (zd->zstat)                                                                //  dialog finish
   {
      freeMouse();
      if (zd->zstat == 1) STL_stat = 1;                                          //  user OK or cancel
      else STL_stat = 0;
      return 1;
   }

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

   if (strmatchN(event,"image",5)) {                                             //  image spin button
      nn = event[5] - '0';                                                       //  0 - N
      zdialog_fetch(zd,event,imageR[nn]);                                        //  new image blend ratio
   }

   zdialog_fetch(zd,"mouseRad",mouseRad);                                        //  mouse radius
   zdialog_fetch(zd,"powerCen",powerCen);                                        //  power center
   zdialog_fetch(zd,"powerEdge",powerEdge);                                      //  power edge
   
   for (sum = imx = 0; imx < cimNF; imx++)                                       //  sum image blend factors
      sum += imageR[imx];
   if (sum < 1) sum = 1;

   for (imx = 0; imx < cimNF; imx++)                                             //  set image coefficients (sum = 1)
      imageC[imx] = imageR[imx] / sum;
   
   zdialog_fetch(zd,"mode1",nn);
   if (nn) STL_mode = 1;                                                         //  paint entire image
   else STL_mode = 2;                                                            //  paint within mouse

   if (STL_mode == 1)                                                            //  paint entire image
   {
      Funcbusy(+1);
      STL_paintall();
      Funcbusy(-1);
   }

   return 1;
}


//  function for painting entire image

void STL_paintall()
{
   using namespace STL_names;
   
   void * STL_paintall_thread(void *);
   
   do_wthreads(STL_paintall_thread,NSMP);
   
   Fpaint2();
   
   return;
}


//  working threads

void * STL_paintall_thread(void *arg)
{
   using namespace STL_names;
   
   void   STL_setpixel(int px, int py, float opacity);

   int      index = *((int *) arg);
   int      px, py;

   for (py = index; py < Ehh; py += NSMP)
   for (px = 0; px < Eww; px++)
      STL_setpixel(px,py,1.0);

   return 0;
}


//  STL dialog mouse function

void STL_mousefunc()
{
   using namespace STL_names;
   
   void STL_setpixel(int px, int py, float opacity);

   int      rect;
   int      mx, my, dx, dy, px, py;
   float    radius, pixrad;
   float    opacity, slope;
   
   if (STL_mode != 2) return;                                                    //  not mouse paint mode

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

   radius = mouseRad;                                                            //  paintbrush radius

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

   if (! Mxdrag && ! Mydrag) return;                                             //  motion event
   
   slope = 0.01 * (powerCen - powerEdge) / radius;                               //  100 .. 50  -->  m = -0.5/radius

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

   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 > Eww-1) continue;                                        //  outside image
      if (py < 0 || py > Ehh-1) continue;

      opacity = 0.01 * powerCen - slope * pixrad;                                //  opacity at pixel radius
      opacity *= 0.1;                                                            //  slow down
      STL_setpixel(px,py,opacity);
   }

   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

void STL_setpixel(int px, int py, float opacity)
{
   using namespace STL_names;

   int         imx;
   float       *pix3, *pix1;
   float       R1, G1, B1;
   
   R1 = G1 = B1 = 0;

   for (imx = 0; imx < cimNF; imx++)                                             //  loop input images
   {
      pix1 = PXMpix(cimPXMf[imx],px,py);                                         //  pixel for input image
      R1 += imageC[imx] * pix1[0];                                               //  sum RGB part for this image
      G1 += imageC[imx] * pix1[1];
      B1 += imageC[imx] * pix1[2];
   }

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

   if (opacity == 1.0)
   {
      pix3[0] = R1;
      pix3[1] = G1;
      pix3[2] = B1;
   }
   
   else 
   {
      pix3[0] = opacity * R1 + (1.0 - opacity) * pix3[0];
      pix3[1] = opacity * G1 + (1.0 - opacity) * pix3[1];
      pix3[2] = opacity * B1 + (1.0 - opacity) * pix3[2];
   }
   
   return;
}


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

   Stack/Split function
   Stack two 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_split();
int    STS_stat;
int    Pww, Phh, P, PP;
int    Finit;

editfunc    EFsts;                                                               //  edit function data


//  menu function

void m_stack_split(GtkWidget *, ch *menu)
{
   int     err;

   F1_help_topic = "stack/split";

   Plog(1,"m_stack_split \n");

   if (CEF) {
      zmessageACK(Mwin,"finish or cancel pending edit");                         //  24.40
      return;
   }

   if (Fblock("stack/split")) return;                                            //  check for blocking function           24.40
   Fblock(0);

   cimNF = cim_get_files(2,2);                                                   //  get 2 files
   if (! cimNF) return;

   if (! cim_load_files()) return;                                               //  load and check all files
   
   if (! cim_check_files(0.0)) return;                                           //  check files are compatible

   free_resources();                                                             //  ready to commit

   Pww = cimPXMf[0]->ww;
   Phh = cimPXMf[0]->hh;

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

   EFsts.menufunc = m_stack_split;
   EFsts.menuname = "Stack/Split";
   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;
   cim_add_meta_hist();
   edit_done(0);
   goto cleanup;

cancel:
   edit_cancel(0);

cleanup:

   cim_free_memory();                                                            //  free memory for input files
   return;
}


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

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

   zdialog  *zd;

/***
       _____________________________
      |     Split two Images        |
      |                             |
      |    drag image boundary      |
      |                             |
      |                    [OK] [X] |
      |_____________________________|
      
***/

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

   P = PP = Pww / 2;                                                             //  initial 50/50 split
   Finit = 1;
   STS_split();

   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, ch *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...Pww-1
   STS_split();

   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_split()
{
   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 < Phh; 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 < Pww) {
               pix3 = PXMpix(E3pxm,px,py);
               memcpy(pix3,red,pcc);
            }
         }

         px = P+Rww;                                                             //  right image
         cc = Pww - 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 < Phh; py++)
   {
      if (P > PP)                                                                //  movement to right
      {
         px = PP - 2 * PRww;
         cc = P - PP + 4 * PRww;
         if (px < 0) px = 0;
         if (px + cc > Pww-1) cc = Pww-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 > Pww-1) cc = Pww-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 < Pww) {
            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 > Pww-1) cc = Pww-1 - px;
   
   Fpaint3(px,0,cc,Phh,0);                                                       //  paint new exposed area
   return;
}


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

    Panorama function: join 2-6 images horizontally

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

int      panStat;                                                                //  1 = OK
zdialog  *panozd = 0;                                                            //  pre-align dialog
float    pano_lens_mm = 30;                                                      //  lens focal length (35mm equiv.) (default) 
float    pano_image_mm;                                                          //  lens focal length at image scale
int      FpanoNC;                                                                //  pano no-curve flag (scanned image)

float panPreAlignSize = 1400;                                                    //  pre-align image size (ww)
float panPreAlignBlend = 0.20;                                                   //  pre-align blend width * ww
float panInitBlend = 0.20;                                                       //  initial auto-align blend width
float panFinalBlend = 0.08;                                                      //  min. final blend width * ww 
float panBlendDecrease = 0.7;                                                    //  BW reduction per align cycle

float pano_adjust_brite[6];                                                      //  user brightness 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[6], *panowarpy[6];                                              //  warp memory, pixel displacements
PXM   *cimPXMwdup[6];                                                            //  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_curve_image(int im);                                                  //  curve images using pano_lens_mm
void  pano_adjust();                                                             //  user color adjust dialog

void  pano_adjust_colors(PXM *pxm, int fwhich);                                  //  adjust pano colors from match data
void  pano_flatten_image(float F);                                               //  flatten panorama image
void  panowarp_mousefunc();                                                      //  image warp mouse function 
void  panowarp_dowarps(int imx);

editfunc EFpano;                                                                 //  edit function data


//  menu function

void m_panorama(GtkWidget *, ch *menu)
{
   int      err;

   F1_help_topic = "panorama";

   Plog(1,"m_panorama \n");

   if (CEF) {
      zmessageACK(Mwin,"finish or cancel pending edit");                         //  24.40
      return;
   }

   if (Fblock("panorama")) return;                                               //  check for blocking function           24.40
   Fblock(0);

   cimNF = cim_get_files(2,6);                                                   //  get 2-6 files
   if (! cimNF) return;

   if (! cim_load_files()) return;                                               //  load and check all files
   
   free_resources();                                                             //  ready to commit

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

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

   EFpano.menufunc = m_panorama;
   EFpano.menuname = "Panorama";
   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

   Fescape = 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;
   cim_add_meta_hist();
   edit_done(0);
   goto cleanup;

cancel:                                                                          //  failed or canceled
   edit_cancel(0);
   if (Fescape)                                                                  //  user cancel 
      zmessage_post(Mwin,"parent",3,"function canceled");

cleanup:

   cim_free_memory();                                                            //  free memory for input files
   *paneltext = 0;
   Fescape = 0;
   return;
}


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

void pano_prealign()
{
   int   pano_prealign_event(zdialog *zd, ch *event);                            //  dialog event function
   void  pano_prealign_mousefunc();

   int         imx, ww, err = 0;
   ch          *metakey[2] = { meta_fl_35_key, meta_fl_key };
   ch          *pp[2] = { 0, 0 };
   ch          *lens_source;
   float       temp;

   ch  *align_mess = "Drag images into rough alignment.\n"
                        "To rotate, drag from lower edge.";
   ch  *scan_mess = "no curve (scanned image)";

   err = 1;
   lens_source = "(no metadata)";
   meta_get1(curr_file,(ch **) metakey,pp,2);                                    //  get lens mm from metadata if available
   if (pp[0]) err = convSF(pp[0], temp, 20, 1000);                               //  try both keys
   else if (pp[1]) err = convSF(pp[1], temp, 20, 1000);
   if (! err) {
      pano_lens_mm = temp;
      lens_source = "(metadata)";
   }

   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
      pano_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 (metadata)          |
      | [x] no curve (scanned image)         |
      | [x] no auto warp                     |
      | [x] manual align                     |
      | Press F1 for help                    |
      |                                      |
      |                        [Proceed] [X] |
      |______________________________________|

*/

   panozd = zdialog_new("Pre-align Images",Mwin,"Proceed"," X ",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","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","no auto warp","space=5");
   zdialog_add_widget(panozd,"hbox","hbma","dialog");
   zdialog_add_widget(panozd,"check","manalign","hbma","manual align","space=5");
   zdialog_add_widget(panozd,"hbox","hbf1","dialog",0,"space=5");
   zdialog_add_widget(panozd,"label","labf1","hbf1","Press F1 for help","space=5");

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

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

   panStat = -1;                                                                 //  busy status
   zdialog_run(panozd,pano_prealign_event,"save");                               //  start dialog
   takeMouse(pano_prealign_mousefunc,dragcursor);                                //  connect mouse
   zdialog_wait(panozd);                                                         //  wait for dialog completion
   zdialog_free(panozd);                                                         //  free dialog
   freeMouse();
   return;
}


//  pre-align dialog event function

int pano_prealign_event(zdialog *zd, ch *event)
{ 
   int      imx, fchange = 0;
   float    overlap;
   ch       *metakey[1] = { meta_fl_35_key };
   ch       *metadata[1];
   ch       lensdata[8];

   if (strmatch(event,"spmm")) {
      zdialog_fetch(zd,"spmm",pano_lens_mm);                                     //  get revised lens data
      fchange = 1;
   }

   if (strmatch(event,"nocurve")) {
      zdialog_fetch(zd,"nocurve",FpanoNC);                                       //  get "no-curve" option
      fchange = 1;
   }

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

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

   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,"save")) {                                                 //  put lens data into dialog
      zdialog_stuff(zd,"spmm",pano_lens_mm);
      snprintf(lensdata,8,"%d",int(pano_lens_mm));
      metadata[0] = lensdata;
      for (imx = 0; imx < cimNF; imx++)                                          //  save lens mm in metadata
         meta_put(cimFile[imx],(ch **) metakey,metadata,1);
   }

   if (zd->zstat)                                                                //  dialog complete
   {
      if (zd->zstat != 1) {                                                      //  cancel or other
         panStat = 0;
         return 1;
      }

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

      for (imx = 0; imx < cimNF-1; imx++)                                        //  check for enough overlap
      {
         overlap = cim_get_overlap(imx,imx+1,cimPXMs);
         if (overlap < panFinalBlend && ! cim_manualalign) {
            zmessageACK(Mwin,"Too little overlap, cannot align");
            panStat = 0;
            return 1;
         }
      }
      
      panStat = 1;
   }

   return 1;
}


//  pre-align mouse function
//  convert mouse and KB events into image movements

void pano_prealign_mousefunc()
{
   cimoffs     offstemp;
   PXM         *pxmtemp;
   ch          *ftemp;
   static int  im1, im2, imm, imx;
   static int  mx0, my0, mx, my;                                                 //  mouse drag origin, position
   static int  xoff, yoff, lox, hix;
   static int  hh, rotate, midx;
   int         sepx, minsep = 9999;
   float       dx, dy, t1, t2, dt;
   
   cimBlend = 0;                                                                 //  full blend during pre-align

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

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

      Mxdrag = Mydrag = 0;

      if (! mx0 && ! my0)                                                        //  new drag
      {
         mx0 = mx;                                                               //  set drag origin
         my0 = my;

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

   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;

   Funcbusy(+1);

   cimAlignSize = 200;                                                           //  initial alignment image size 

   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
      pano_curve_image(imx);                                                     //  curve them   PXMs > PXMs
   }
   
   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);
   pano_adjust_colors(cimPXMf[0],1);                                             //  image 0 << profile 1
   pano_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);
      pano_adjust_colors(cimPXMf[0],1);
      pano_adjust_colors(cimPXMf[1],1);
      pano_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);
      pano_adjust_colors(cimPXMf[0],1);
      pano_adjust_colors(cimPXMf[1],1);
      pano_adjust_colors(cimPXMf[2],1);
      pano_adjust_colors(cimPXMf[3],2);
   }

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

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

   cimScale = cimAlignSize / 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;
   }

   cimImageIncrease = 1.6;                                                       //  size increase per align stage
   cimSampSize = 5000;                                                           //  alignment pixel sample size
   cimSearchRange = 16;                                                          //  initial alignment search range
   cimMinSearchRange = 4;                                                        //  minimum search range
   cimSearchStep = 1;                                                            //  alignment search step
   cimWarpRange = 16;                                                            //  initial alignment warp range
   cimMinWarpRange = 4;                                                          //  minimum warp range
   cimWarpStep = 1;                                                              //  alignment warp step

   ww = cimPXMf[0]->ww * cimScale;                                               //  initial align image width
   cimBlend = ww * panInitBlend;                                                 //  initial align blend width

   cimNsearch = 0;                                                               //  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   PXMf > PXMs
         pano_curve_image(imx);                                                  //  curve based on lens_mm  PXMs > PXMs
         cim_warp_image_pano(imx,1);                                             //  apply corner warps  PXMs > PXMw
      }

      m_zoom(0,"fit");                                                           //  fit window

      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-3 && ! cim_manualalign) {                   //  extra margin for align steps 
            zmessageACK(Mwin,"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_images(im1,im2);                                           //  search for best offsets and warps
            zfree(cimRedpix);                                                    //  clear red pixels
            cimRedpix = 0;
            if (Fescape)  goto fail;                                             //  user kill
         }

         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 = cimImageIncrease;                                                      //  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 = cimSearchRange / cimImageIncrease + 0.5;
      if (cimSearchRange < cimMinSearchRange) cimSearchRange = cimMinSearchRange;
      cimWarpRange = cimWarpRange / cimImageIncrease + 0.5;
      if (cimWarpRange < cimMinWarpRange) cimWarpRange = cimMinWarpRange;

      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)
   Funcbusy(-1);
   cim_show_images(0,0);
   return;
}


//  curve image based on pano_lens_mm
//  replaces cimPXMs[im] with curved version

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

   if (FpanoNC) 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;

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


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

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

   ch       *adjusttitle = "Match Brightness";
   ch       imageN[8] = "imageN";
   int      ww, hh, cc, imx;

   for (imx = 0; imx < cimNF; imx++)                                             //  neutral color adjustments
      pano_adjust_brite[imx] = 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,"cim pano");
      panowarpy[imx] = (float *) zmalloc(cc,"cim pano");
      cimPXMwdup[imx] = PXM_copy(cimPXMw[imx]);
   }
   
/***
       ___________________________________
      |       Match Brightness            |
      |                                   |
      |  Select Image (o) (o) (o) (o)     |        image0 image1 ...
      |  Brightness [____]  [apply]       |        setbrite       applybrite
      |  [auto color]  [file color]       |        autocolor      filecolor
      |  Blend Width [____] [apply]       |        blendwidth     applyblend
      |  Flatten Pano [___] [apply]       |        setflatten     applyflatten
      |  [x] Mouse Warp (drag image)      |        mousewarp
      |                                   |
      |                          [OK] [X] |
      |___________________________________|

***/

   panozd = zdialog_new(adjusttitle,Mwin,"OK"," X ",null);

   zdialog_add_widget(panozd,"hbox","hbsel","dialog",0,"space=3");
   zdialog_add_widget(panozd,"label","labsel","hbsel","Select Image","space=3");

   zdialog_add_widget(panozd,"hbox","hbbri","dialog",0,"space=1");
   zdialog_add_widget(panozd,"label","labbr","hbbri","Brightness","space=3");
   zdialog_add_widget(panozd,"zspin","setbrite","hbbri","50|200|1|100");
   zdialog_add_widget(panozd,"button","applybrite","hbbri","apply","space=10");
   
   zdialog_add_widget(panozd,"hsep","hsep","dialog",0,"space=3");
   zdialog_add_widget(panozd,"hbox","hbcolor","dialog",0,"space=3");
   zdialog_add_widget(panozd,"button","autocolor","hbcolor","auto color","space=5");
   zdialog_add_widget(panozd,"button","filecolor","hbcolor","file color","space=5");

   zdialog_add_widget(panozd,"hbox","hbblend","dialog",0,"space=1");
   zdialog_add_widget(panozd,"label","labblend","hbblend","Blend Width","space=3");
   zdialog_add_widget(panozd,"zspin","blendwidth","hbblend","1|999|1|1");
   zdialog_add_widget(panozd,"button","applyblend","hbblend","apply","space=10");

   zdialog_add_widget(panozd,"hbox","hbflat","dialog",0,"space=1");
   zdialog_add_widget(panozd,"label","labflat","hbflat","Flatten Pano","space=3");
   zdialog_add_widget(panozd,"zspin","setflatten","hbflat","0|1|0.1|0","space=3");
   zdialog_add_widget(panozd,"button","applyflatten","hbflat","apply","space=10");

   zdialog_add_widget(panozd,"hbox","hbwarp","dialog",0,"space=1");
   zdialog_add_widget(panozd,"check","mousewarp","hbwarp","Mouse Warp (drag image)","space=3");

   zdialog_add_ttip(panozd,"labsel","select base image for color matching");     //  24.10
   zdialog_add_ttip(panozd,"labbr","adjust brightness of selected image");
   zdialog_add_ttip(panozd,"autocolor","match image color to base image (repeat)");
   zdialog_add_ttip(panozd,"filecolor","set image color from input files");
   zdialog_add_ttip(panozd,"labblend","blend image overlaps over pixel width");
   zdialog_add_ttip(panozd,"labflat","remove panorama image curving");
   zdialog_add_ttip(panozd,"mousewarp","warp and fine-align selected image with neighbor");

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

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

   cimBlend = 1;
   zdialog_stuff(panozd,"blendwidth",cimBlend);                                  //  bugfix   24.30

   preflatPXM = 0;

   panStat = -1;                                                                 //  busy status
   zdialog_resize(panozd,400,0);
   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, ch *event)
{
   ch          imageN[8] = "imageN";
   int         imS, imx, im1, im2;
   int         ww, hh, px, py, nn;
   float       F, brite, R, G, B;
   float       *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]);
         zfree(panowarpx[imx]);
         zfree(panowarpy[imx]);
      }
      if (preflatPXM) PXM_free(preflatPXM);                                      //  free memory
      preflatPXM = 0;
      return 1;
   }

   if (strstr(event,"image"))                                                    //  new image selected
   {
      imx = event[5] - '0';                                                      //  image 0 - cimNF-1
      if (imx < 0 || imx >= cimNF) return 1;
      brite = pano_adjust_brite[imx];                                            //  set image brightness in dialog
      zdialog_stuff(zd,"setbrite",brite);
      pano_currimage = imx;
   }

   for (imx = 0; imx < cimNF; imx++) {                                           //  get currently selected image
      imageN[5] = '0' + imx;                                                     //    from radio buttons
      zdialog_fetch(zd,imageN,nn);
      if (nn) break;
   }
   if (imx == cimNF) return 1;                                                   //  none selected
   pano_currimage = imx;

   if (strmatch(event,"applybrite"))                                             //  new image brightness adjustment
   {
      zdialog_fetch(zd,"setbrite",brite);                                        //  new brightness value
      pano_adjust_brite[imx] = brite;                                            //  update image brightness

      if (preflatPXM) {                                                          //  unflatten image if needed
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

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

      F = brite / 100;                                                           //  normalize 0.5 - 2.0      
      
      ww = cimPXMw[imx]->ww;
      hh = cimPXMw[imx]->hh;

      for (py = 0; py < hh; py++)                                                //  loop all image pixels
      for (px = 0; px < ww; px++)
      {
         pixel = PXMpix(cimPXMw[imx],px,py);
         R = F * pixel[0];                                                       //  apply brightness factor
         G = F * pixel[1];
         B = F * pixel[2];

         RGBfix(R,G,B);

         pixel[0] = R;
         pixel[1] = G;
         pixel[2] = B;
      }

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


   if (strmatch(event,"autocolor"))
   {
      Funcbusy(+1,1);

      if (preflatPXM) {
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }
      
      imS = pano_currimage;                                                      //  last image selected
      
      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);
         pano_adjust_colors(cimPXMw[im1],1);                                     //  image im1 << profile 1
         pano_adjust_colors(cimPXMw[im2],2);                                     //  image im2 << profile 2
         for (imx = im1-1; imx >= imS; imx--)
            pano_adjust_colors(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);
         pano_adjust_colors(cimPXMw[im1],1);                                     //  image im1 << profile 1
         pano_adjust_colors(cimPXMw[im2],2);                                     //  image im2 << profile 2
         for (imx = im2+1; imx < cimNF; imx++)
            pano_adjust_colors(cimPXMw[imx],2);
      }

      cimBlend = 1;
      zdialog_stuff(zd,"blendwidth",cimBlend);
      cim_show_images(0,0);
      
      Funcbusy(-1);
   }

   if (strmatch(event,"filecolor"))                                              //  use original file colors
   {
      Funcbusy(+1,1);

      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]);
         pano_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,"blendwidth",cimBlend);

      cim_show_images(0,0);

      Funcbusy(-1);
   }

   if (strmatch(event,"applyblend"))                                             //  apply new blend width
   {
      if (preflatPXM) {                                                          //  unflatten image if needed
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

      zdialog_fetch(zd,"blendwidth",cimBlend);                                   //  can be zero
      cim_show_images(0,1);                                                      //  show with gradual blend
   }

   if (strmatch(event,"mousewarp"))                                              //  engage mouse warp/align
   {
      if (preflatPXM) {                                                          //  unflatten image if needed
         PXM_free(E3pxm);
         E3pxm = preflatPXM;
         preflatPXM = 0;
      }

      cimBlend = 1;
      zdialog_stuff(zd,"blendwidth",cimBlend);                                   //  set blendwidth = 1
      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,"applyflatten"))
   {
      if (preflatPXM) {                                                          //  E3 = flattened image
         PXM_free(E3pxm);
         E3pxm = PXM_copy(preflatPXM);
      }
      else preflatPXM = PXM_copy(E3pxm);                                         //  flattened image = E3

      zdialog_fetch(zd,"setflatten",F);                                          //  0 ... 0.40 0.50 0.60 0.70 0.80 0.90 1.0
      F = sqrtf(F);                                                              //  0 ... 0.63 0.71 0.77 0.84 0.89 0.95 1.0
      pano_flatten_image(F);                                                     //  flatten image
      
      Fpaint2();
   }

   return 1;
}


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

void pano_flatten_image(float F)
{
   PXM      *pxmout;
   int      ww, hh, vstat;
   float    ww2, hh2;
   int      px, py, rx, ry;
   int      xlo, xhi, ylo, yhi;
   float    dx, dy;
   float    sx, sy;
   float    tx, ty;
   float    *pix, vpix[4];
   float    *pix1, *pix2;
   
   Funcbusy(+1);
                                                                                 //  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 * pano_image_mm;                                                        //      8*FL (min flat) to 1*FL (max flat)

   ww = E3pxm->ww;
   hh = E3pxm->hh;
   ww2 = ww * 1.4;
   hh2 = hh * 1.2;
   
   pxmout = PXM_make(ww2,hh2,4);                                                 //  larger image space
   
   for (ry = 0; ry < hh; ry++)                                                   //  copy image to middle
   for (rx = 0; rx < ww; rx++)
   {
      px = rx + 0.2 * ww;
      if (px > ww2-1) continue;
      py = ry + 0.1 * hh;
      if (py > hh2-1) continue;
      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

   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;
   
   ww = E3pxm->ww;                                                               //  cut off excess margins
   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);

   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;
   
   Funcbusy(-1);
   return;
}


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

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


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

//  show files selected for present or last combine edit function

void m_cim_show_files(GtkWidget *, ch *)
{
   F1_help_topic = "show CIM files";

   zdialog  *zd;
   
   if (! cimNF) {
      zmessageACK(Mwin,"no input files available");
      return;
   }
   
   zd = popup_report_open("combined image files",Mwin,600,200,0,0,0,"X",0);
   
   for (int im = 0; im < cimNF; im++)
      popup_report_write(zd,0,"%s\n",cimFile[im]);
   
   return;
}


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

//  select input files for combine image function
//  returns count of files selected and loaded

int cim_get_files(int min, int max)
{
   int      im;

   cimNF = 0;

   for (im = 0; im < 10; im++)                                                   //  clear previous file list
   {
      if (cimFile[im]) zfree(cimFile[im]);
      cimFile[im] = 0;
   }

   select_files(1);                                                              //  get new list
   if (SFcount == 0) return 0;
   
   if (SFcount < min || SFcount > max) {
      zmessageACK(Mwin,"Select %d to %d files",min,max);
      return 0;
   }

   cimNF = SFcount;                                                              //  file count

   for (im = 0; im < cimNF; im++)
      cimFile[im] = zstrdup(SelFiles[im],"cim get files");                       //  set up file list

   cimLF = 0;                                                                    //  find alphabetically last file
   for (int im = 1; im < cimNF; im++)
      if (strcmp(cimFile[im],cimFile[cimLF]) > 0) cimLF = im;

   return cimNF;
}


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

int cim_load_files()
{
   PXM      *pxm;
   
   for (int im = 0; im < cimNF; im++)
   {
      PXM_free(cimPXMf[im]);
      pxm = PXM_load(cimFile[im],1);                                             //  ACK errors
      if (! pxm) {
         cim_free_memory();
         return 0;
      }
      PXM_addalpha(pxm);
      cimPXMf[im] = pxm;
      zmainloop();                                                               //  deter GTK "not responding"
   }
   
   return 1;
}


//  check input files for compatibility
//  returns 0 if not compatible

int cim_check_files(float maxdiff)
{
   int      ww, hh, im;
   float    diffw, diffh;

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

   for (im = 1; im < cimNF; im++)
   {
      diffw = abs(ww - cimPXMf[im]->ww);
      diffw = diffw / ww;
      diffh = abs(hh - cimPXMf[im]->hh);
      diffh = diffh / hh;

      if (diffw > maxdiff || diffh > maxdiff) {
         zmessageACK(Mwin,"Images sizes are not compatible");
         cim_free_memory();
         return 0;
      }
   }
   
   return 1;
}


//  free memory used by cim file PXM images
//  file names are not freed - for use by m_cim_files()

void cim_free_memory()
{
   for (int im = 0; im < 10; im++) 
   {
      if (cimPXMf[im]) PXM_free(cimPXMf[im]);
      if (cimPXMs[im]) PXM_free(cimPXMs[im]);
      if (cimPXMw[im]) PXM_free(cimPXMw[im]);
      cimPXMf[im] = cimPXMs[im] = cimPXMw[im] = 0;
   }

   return;
}


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

   return 1.0 * (cimOv1xhi - cimOv1xlo) / ww1;                                   //  return overlap width <= 1.0
}


//  Get the RGB brightness histogram 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         brhist1[3][256], brhist2[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 histograms
   for (ii = 0; ii < 256; ii++)
      brhist1[rgb][ii] = brhist2[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 histogram
      {                                                                          //    by color in 256 bins
         ++brhist1[rgb][int(pix1[rgb])];
         ++brhist2[rgb][int(vpix2[rgb])];
      }
   }

   if (npix < 2560) {
      zmessageACK(Mwin,"insufficient overlap for color matching");
      for (ii = 0; ii < 256; ii++) 
      for (rgb = 0; rgb < 3; rgb++)                                              //  neutralize color match data
         cimRGBmf1[rgb][ii] = cimRGBmf2[rgb][ii] = ii;
      return;
   }

   npix1 = npix / 256;                                                           //  1/256th of overlapping 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 histogram
      {
         npix3 = brhist1[rgb][ii];
         if (npix3 == 0) { ++ii; continue; }
         if (npix3 > npix2) npix3 = npix2;
         brlev1[rgb][jj] += ii * npix3;                                          //  brightness * (pixels with)
         brhist1[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 = brhist2[rgb][ii];
         if (npix3 == 0) { ++ii; continue; }
         if (npix3 > npix2) npix3 = npix2;
         brlev2[rgb][jj] += ii * npix3;
         brhist2[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 (isnan(bratio)) bratio = 1;                                             //  NAN detector
      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 (isnan(bratio)) bratio = 1;                                             //  NAN detector
      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;
   float       *pix;

   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 (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) red = 255;
      if (green > 255) green = 255;
      if (blue > 255) blue = 255;

      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 pano_adjust_colors(PXM *pxm, int fwhich)
{
   int         ww, hh, ww2, px, py;
   int         pxlo, pxhi;
   float       red1, green1, blue1;
   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 (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) red2 = 255;                                                //  instead of RGBFIX
      if (green2 > 255) green2 = 255;
      if (blue2 > 255) blue2 = 255;

      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         Hhist[256], Vhist[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
   uch         *Hcon, *Vcon;
   float       *pix1, *pix2;
   PXM         *pxm;
   
   pxm = cimPXMs[im1];
   ww = pxm->ww;
   hh = pxm->hh;

   if (cimRedpix) zfree(cimRedpix);                                              //  clear prior
   cimRedpix = (ch *) zmalloc(ww * hh,"cim redpix");

   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;
      Hcon = (uch *) zmalloc(npix,"cim redpix");                                 //  horizontal pixel contrast 0-255
      Vcon = (uch *) zmalloc(npix,"cim redpix");                                 //  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
      if (samp > 500) samp = 500;

      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 histogram
         Hhist[ii] = Vhist[ii] = 0;

      for (py = pyL; py < pyH; py++)                                             //  scan image pixels
      for (px = pxL; px < pxH; px++)
      {                                                                          //  build contrast histogram
         ii = (py-pyL) * (pxH-pxL) + (px-pxL);
         ++Hhist[Hcon[ii]];
         ++Vhist[Vcon[ii]];
      }

      for (npix = 0, ii = 255; ii > 0; ii--)                                     //  find minimum contrast needed to get
      {                                                                          //    enough pixels for sample size
         npix += Hhist[ii];                                                      //      (horizontal contrast pixels)
         if (npix > samp) break;
      }
      Hmin = ii;

      for (npix = 0, ii = 255; ii > 0; ii--)                                     //  (vertical contrast pixels)
      {
         npix += Vhist[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;
}


//  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,NSMP);                                     //  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 += NSMP)                                      //  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
   }

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


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

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

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

         if (Fescape)  return;                                                   //  user kill
      }

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

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

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

            if (Fescape) return;                                                 //  user kill
         }

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

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

   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;                                                        //  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 pixels
         cmatch += match;                                                        //  accumulate total match
         maxcmatch += 1.0;
      }
   }

   return cmatch / maxcmatch;
}


//  combine and show all images
//  used for all combine 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

   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
      Eww = wwmax; 
      Ehh = 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,NSMP);                                    //  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(-1);                                                                //  unblock window paint
   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 += NSMP)                               //  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;
   }

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


//  add list of used image files to metadata history

void cim_add_meta_hist()
{
   int      ii, hcc, pcc;
   ch       *pp, imagenames[1000];
   
   for (ii = hcc = 0; ii < cimNF; ii++)
   {
      pp = strrchr(cimFile[ii],'/');
      if (! pp) zappcrash("cim file missing: %d",ii);
      pp++;
      pcc = strlen(pp);
      if (hcc + pcc >= 998) Plog(0,"cannot add meta history \n");
      strcpy(imagenames+hcc,pp);
      hcc += pcc;
      strcpy(imagenames+hcc," ");
      hcc++;
   }
   
   edit_addhist(imagenames);
   
   return;
}


//  dump offsets to stdout - diagnostic tool

void cim_dump_offsets(ch *label)
{
   Plog(1,"\n offsets: %s \n",label);

   for (int imx = 0; imx < cimNF; imx++)
   {
      Plog(1," 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;
}



