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

   Fotoxx      edit photos and manage collections

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

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

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
   See the GNU General Public License for more details.

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

   Fotoxx image edit - Enhance menu functions

   m_voodoo1               automatic image enhancement with limited smarts
   m_voodoo2               automatic image enhancement with limited smarts
   m_brite_dist            edit brightness distribution, rebalance bright/dark areas
   m_gradients             magnify brightness gradients to enhance details
   m_flatten               flatten brightness distribution
   m_gretinex              rescale pixel RGB brightness range - for global image
   m_zretinex              rescale pixel RGB brightness range - for image zones
   m_sharpen               sharpen an image
   m_blur                  blur an image
   m_blur_background       blur background (called via m_blur())
   m_denoise               remove noise from an image
   m_redeyes               remove red-eyes from flash photos
   m_match_colors          adjust image colors to match those in a chosen image
   m_smart_erase           replace pixels inside selected areas with background
   m_chromatic1            fix lateral CA (color bands increasing with radius)
   m_chromatic2            fix axial CA (color bands on dark/bright edges)
   m_vignette              highlight selected image region
   m_remove_dust           remove dust specs from an image

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

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

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

//  automatic image tuneup without user guidance

float       voodoo1_brdist[256];                                                 //  pixel count per brightness level
int         voodoo_01, voodoo_99;                                                //  1% and 99% brightness levels (0 - 255)
void        voodoo1_distribution();                                              //  compute brightness distribution
void *      voodoo1_thread(void *);
editfunc    EFvoodoo1;


void m_voodoo1(GtkWidget *, cchar *menu)
{
   F1_help_topic = "voodoo 1";

   EFvoodoo1.menuname = menu;
   EFvoodoo1.menufunc = m_voodoo1;
   EFvoodoo1.funcname = "voodoo1";
   EFvoodoo1.Farea = 1;                                                          //  select area ignored
   EFvoodoo1.Fscript = 1;                                                        //  scripting supported
   EFvoodoo1.threadfunc = voodoo1_thread;

   if (! edit_setup(EFvoodoo1)) return;                                          //  setup edit
   voodoo1_distribution();                                                       //  compute brightness distribution
   signal_thread();                                                              //  start update thread
   edit_done(0);                                                                 //  edit done
   return;
}
   

//  compute brightness distribution for image

void voodoo1_distribution()
{
   int         px, py, ii;
   int         ww = E1pxm->ww, hh = E1pxm->hh;
   float       bright1;
   float       *pix1;

   for (ii = 0; ii < 256; ii++)                                                  //  clear brightness distribution data
      voodoo1_brdist[ii] = 0;

   for (py = 0; py < hh; py++)                                                   //  compute brightness distribution
   for (px = 0; px < ww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);
      bright1 = pixbright(pix1);
      voodoo1_brdist[int(bright1)]++;
   }

   for (ii = 1; ii < 256; ii++)                                                  //  cumulative brightness distribution
      voodoo1_brdist[ii] += voodoo1_brdist[ii-1];                                //   0 ... (ww * hh)

   voodoo_01 = 0;
   voodoo_99 = 255;

   for (ii = 0; ii < 256; ii++)                                                  //  compute index values (0 - 255)
   {                                                                             //    for darkest 1% and brightest 1%
      if (voodoo1_brdist[ii] < 0.01 * ww * hh) voodoo_01 = ii;
      if (voodoo1_brdist[ii] < 0.99 * ww * hh) voodoo_99 = ii;
   }

   for (ii = 0; ii < 256; ii++)
      voodoo1_brdist[ii] = voodoo1_brdist[ii]                                    //  multiplier per brightness level
                    / (ww * hh) * 256.0 / (ii + 1);                              //  ( = 1 for a flat distribution)
   return;
}


//  thread function - use multiple working threads

void * voodoo1_thread(void *)
{
   void  * voodoo1_wthread(void *arg);

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

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

      do_wthreads(voodoo1_wthread,NWT);

      CEF->Fmods++;
      CEF->Fsaved = 0;

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

   return 0;                                                                     //  not executed, stop g++ warning
}

void * voodoo1_wthread(void *arg)                                                //  worker thread function
{
   int         index = *((int *) (arg));
   int         px, py;
   float       *pix1, *pix3;
   float       bright1, bright2, bright3, cmax;
   float       red1, green1, blue1, red3, green3, blue3;
   float       flat1 = 0.3;                                                      //  strength of distribution flatten
   float       flat2 = 1.0 - flat1;
   float       sat1 = 0.3, sat2;                                                 //  strength of saturation increase
   float       f1, f2;
   float       expand = 256.0 / (voodoo_99 - voodoo_01 + 1);                     //  brightness range expander

   for (py = index; py < E1pxm->hh; py += NWT)                                   //  voodoo brightness distribution
   for (px = 0; px < E1pxm->ww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

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

      bright2 = 0.25 * red1 + 0.65 * green1 + 0.10 * blue1;                      //  input brightness, 0 - 256
      bright2 = (bright2 - voodoo_01) * expand;                                  //  expand to clip low / high 1%
      if (bright2 < 0) bright2 = 0;
      if (bright2 > 255) bright2 = 255;

      bright1 = voodoo1_brdist[int(bright2)];                                    //  factor for flat output brightness
      bright3 = flat1 * bright1 + flat2;                                         //  attenuate

      f1 = (256.0 - bright2) / 256.0;                                            //  bright2 = 0 - 256  >>  f1 = 1 - 0
      f2 = 1.0 - f1;                                                             //  prevent banding in bright areas
      bright3 = f1 * bright3 + f2;                                               //  tends to 1.0 for brighter pixels

      red3 = red1 * bright3;                                                     //  blend new and old brightness
      green3 = green1 * bright3;
      blue3 = blue1 * bright3;

      bright3 = 0.333 * (red3 + green3 + blue3);                                 //  mean color brightness
      sat2 = sat1 * (256.0 - bright3) / 256.0;                                   //  bright3 = 0 - 256  >>  sat2 = sat1 - 0
      red3 = red3 + sat2 * (red3 - bright3);                                     //  amplified color, max for dark pixels
      green3 = green3 + sat2 * (green3 - bright3);
      blue3 = blue3 + sat2 * (blue3 - bright3);

      if (red3 < 0) red3 = 0;                                                    //  stop possible underflow
      if (green3 < 0) green3 = 0;
      if (blue3 < 0) blue3 = 0;

      cmax = red3;                                                               //  stop overflow, keep color balance
      if (green3 > cmax) cmax = green3;
      if (blue3 > cmax) cmax = blue3;
      if (cmax > 255.9) {
         cmax = 255.9 / cmax;
         red3 = red3 * cmax;
         green3 = green3 * cmax;
         blue3 = blue3 * cmax;
      }

      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

   pthread_exit(0);
}


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

//  flatten brightness distribution based on the distribution of nearby zones

namespace voodoo2_names
{
   float    flatten = 30;                          //  flatten value, 30%
   float    deband = 70;                           //  deband value, 70%
   int      Iww, Ihh;                              //  image dimensions
   int      NZ;                                    //  no. of image zones
   int      Zsize, Zrows, Zcols;                   //  zone parameters
   float    *Br;                                   //  Br[py][px]  pixel brightness
   int      *Zxlo, *Zylo, *Zxhi, *Zyhi;            //  Zxlo[ii] etc.  zone ii pixel range
   int      *Zcen;                                 //  Zcen[ii][2]  zone ii center (py,px)
   uint8    *Zn;                                   //  Zn[py][px][9]  9 nearest zones for pixel: 0-200 (255 = none)
   uint8    *Zw;                                   //  Zw[py][px][9]  zone weights for pixel: 0-100 = 1.0
   float    *Zff;                                  //  Zff[ii][1000]  zone ii flatten factors

   editfunc EFvoodoo2;                             //  edit function
   void * wthread(void *);                         //  working thread
}


void m_voodoo2(GtkWidget *, cchar *menu)
{
   using namespace voodoo2_names;

   int      gNZ, pNZ;
   int      px, py, cx, cy;
   int      rx, ii, jj, kk;
   int      zww, zhh, row, col;
   float    *pix1, bright;
   float    weight[9], sumweight;
   float    D, Dthresh;
   
   F1_help_topic = "voodoo 2";

   EFvoodoo2.menuname = menu;
   EFvoodoo2.menufunc = m_voodoo2;
   EFvoodoo2.funcname = "voodoo2";
   EFvoodoo2.Farea = 1;                                                          //  select area ignored
   EFvoodoo2.Fscript = 1;                                                        //  scripting supported

   if (! edit_setup(EFvoodoo2)) return;                                          //  setup edit

   Iww = E1pxm->ww;                                                              //  image dimensions
   Ihh = E1pxm->hh;
   
   if (Iww * Ihh > wwhh_limit2) {
      zmessageACK(Mwin,"image too big");
      edit_cancel(0);
      return;
   }

   Ffuncbusy = 1;

   NZ = 40;                                                                      //  zone count
   gNZ = pNZ = NZ;                                                               //  zone count goal

   while (true)
   {
      Zsize = sqrtf(Iww * Ihh / gNZ);                                            //  approx. zone size
      Zrows = Ihh / Zsize + 0.5;                                                 //  get appropriate rows and cols
      Zcols = Iww / Zsize + 0.5;
      NZ = Zrows * Zcols;                                                        //  NZ matching rows x cols
      if (NZ >= pNZ) break;
      gNZ++;
   }

   Br = (float *) zmalloc(Iww * Ihh * sizeof(float));                            //  allocate memory
   Zn = (uint8 *) zmalloc(Iww * Ihh * 9 * sizeof(uint8));                        //  cannot exceed 2048 M
   Zw = (uint8 *) zmalloc(Iww * Ihh * 9 * sizeof(uint8));
   Zxlo = (int *) zmalloc(NZ * sizeof(int));
   Zylo = (int *) zmalloc(NZ * sizeof(int));
   Zxhi = (int *) zmalloc(NZ * sizeof(int));
   Zyhi = (int *) zmalloc(NZ * sizeof(int));
   Zcen = (int *) zmalloc(NZ * 2 * sizeof(int));
   Zff = (float *) zmalloc(NZ * 1000 * sizeof(float));

   for (py = 0; py < Ihh; py++)                                                  //  get initial pixel brightness levels
   for (px = 0; px < Iww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);
      bright = pixbright(pix1);
      ii = py * Iww + px;
      Br[ii] = bright;
      zmainloop(1000);                                                           //  keep GTK alive
   }

   zww = Iww / Zcols;                                                            //  actual zone size
   zhh = Ihh / Zrows;

   for (row = 0; row < Zrows; row++)
   for (col = 0; col < Zcols; col++)                                             //  set low/high bounds per zone
   {
      ii = row * Zcols + col;
      Zxlo[ii] = col * zww;
      Zylo[ii] = row * zhh;
      Zxhi[ii] = Zxlo[ii] + zww;
      Zyhi[ii] = Zylo[ii] + zhh;
      Zcen[2*ii] = Zylo[ii] + zhh/2;
      Zcen[2*ii+1] = Zxlo[ii] + zww/2;
   }

   for (ii = 0; ii < NZ; ii++)                                                   //  compute brightness distributiion
   {                                                                             //    for each zone
      for (jj = 0; jj < 1000; jj++)
         Zff[1000*ii+jj] = 0;

      for (py = Zylo[ii]; py < Zyhi[ii]; py++)                                   //  brightness distribution
      for (px = Zxlo[ii]; px < Zxhi[ii]; px++)
      {
         pix1 = PXMpix(E1pxm,px,py);
         bright = 1000.0 / 256.0 * pixbright(pix1);
         Zff[1000*ii+int(bright)]++;
      }

      for (jj = 1; jj < 1000; jj++)                                              //  cumulative brightness distribution
         Zff[1000*ii+jj] += Zff[1000*ii+jj-1];

      for (jj = 0; jj < 1000; jj++)                                              //  multiplier per brightness level
         Zff[1000*ii+jj] = Zff[1000*ii+jj] / (zww*zhh) * 1000 / (jj+1);          //    to make distribution flat

      zmainloop(1000);                                                           //  keep GTK alive
   }

   for (py = 0; py < Ihh; py++)                                                  //  set 9 nearest zones per pixel
   for (px = 0; px < Iww; px++)
   {
      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      row = py / zhh;                                                            //  zone containing pixel
      col = px / zww;

      ii = 0;
      for (jj = row-1; jj <= row+1; jj++)                                        //  loop 3x3 surrounding zones
      for (kk = col-1; kk <= col+1; kk++)
      {
         if (jj >= 0 && jj < Zrows && kk >= 0 && kk < Zcols)                     //  zone is not off the edge
            Zn[rx+ii] = jj * Zcols + kk;
         else Zn[rx+ii] = 255;                                                   //  edge row/col: missing neighbor
         ii++;
      }
   }

   if (zww < zhh)                                                                //  pixel to zone distance threshold
      Dthresh = 1.5 * zww;                                                       //  area influence = 0 beyond this distance
   else Dthresh = 1.5 * zhh;

   for (py = 0; py < Ihh; py++)                                                  //  set zone weights per pixel
   for (px = 0; px < Iww; px++)
   {
      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      for (ii = 0; ii < 9; ii++)                                                 //  distance to each zone center
      {
         jj = Zn[rx+ii];
         if (jj == 255) {
            weight[ii] = 0;                                                      //  missing zone weight = 0
            continue;
         }
         cy = Zcen[2*jj];
         cx = Zcen[2*jj+1];
         D = sqrtf((py-cy)*(py-cy) + (px-cx)*(px-cx));                           //  distance from pixel to zone center
         D = D / Dthresh;
         if (D > 1) D = 1;                                                       //  zone influence reaches zero at Dthresh
         weight[ii] = 1.0 - D;
      }

      sumweight = 0;
      for (ii = 0; ii < 9; ii++)                                                 //  zone weights based on distance
         sumweight += weight[ii];

      for (ii = 0; ii < 9; ii++)                                                 //  normalize weights, sum = 1.0
         Zw[rx+ii] = 100 * weight[ii] / sumweight;                               //  0-100 = 1.0

      zmainloop(1000);                                                           //  keep GTK alive
   }

   do_wthreads(wthread,NWT);

   zfree(Br);                                                                    //  free memory
   zfree(Zn);
   zfree(Zw);
   zfree(Zxlo);
   zfree(Zylo);
   zfree(Zxhi);
   zfree(Zyhi);
   zfree(Zcen);
   zfree(Zff);

   Ffuncbusy = 0;
   CEF->Fmods++;
   CEF->Fsaved = 0;
   edit_done(0);                                                                 //  edit done
   return;
}


//  worker thread for each CPU processor core

void * voodoo2_names::wthread(void *arg)
{
   using namespace voodoo2_names;

   int         index = *((int *) (arg));
   int         px, py, rx, ii, jj;
   float       *pix1, *pix3;
   float       fold, fnew, cmax;
   float       red1, green1, blue1, red3, green3, blue3;
   float       FF, weight, bright, debandx;

   for (py = index; py < Ihh; py += NWT)                                         //  loop all image pixels
   for (px = 0; px < Iww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

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

      bright = 0.25 * red1 + 0.65 * green1 + 0.10 * blue1;                       //  input pixel brightness 0-255.9
      bright *= 1000.0 / 256.0;                                                  //  0-999

      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      FF = 0;
      for (ii = 0; ii < 9; ii++) {                                               //  loop 9 nearest zones
         weight = Zw[rx+ii];
         if (weight > 0) {                                                       //  0-100 = 1.0
            jj = Zn[rx+ii];
            FF += 0.01 * weight * Zff[1000*jj+int(bright)];                      //  sum weight * flatten factor
         }
      }

      red3 = FF * red1;                                                          //  fully flattened brightness
      green3 = FF * green1;
      blue3 = FF * blue1;

      debandx = (1000.0 - bright) / 1000.0;                                      //  bright = 0 to 1000  >>  debandx = 1 to 0
      debandx += (1.0 - debandx) * (1.0 - 0.01 * deband);                        //  debandx = 1 to 1  >>  1 to 0

      fnew = 0.01 * flatten;                                                     //  how much to flatten, 0 to 1
      fnew = debandx * fnew;                                                     //  attenuate flatten of brighter pixels
      fold = 1.0 - fnew;                                                         //  how much to retain, 1 to 0

      red3 = fnew * red3 + fold * red1;                                          //  blend new and old brightness
      green3 = fnew * green3 + fold * green1;
      blue3 = fnew * blue3 + fold * blue1;

      cmax = red3;                                                               //  stop overflow, keep color balance
      if (green3 > cmax) cmax = green3;
      if (blue3 > cmax) cmax = blue3;
      if (cmax > 255.9) {
         cmax = 255.9 / cmax;
         red3 = red3 * cmax;
         green3 = green3 * cmax;
         blue3 = blue3 * cmax;
      }

      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

   pthread_exit(0);
}


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

//  edit brightness distribution

namespace brite_dist_names
{
   int      ww, hh;
   float    LC, HC;                                                              //  low, high cutoff levels
   float    LF, MF, HF;                                                          //  low, mid, high flatten parms
   float    LS, MS, HS;                                                          //  low, mid, high stretch parms
   float    BB[1000];                                                            //  adjusted B for input B 0-999

   int    dialog_event(zdialog* zd, cchar *event);
   void   compute_BB();
   void * thread(void *);
   void * wthread(void *);

   editfunc    EFbrightdist;
}


//  menu function

void m_brite_dist(GtkWidget *, cchar *menu)
{
   using namespace brite_dist_names;

   cchar  *title = E2X("Adjust Brightness Distribution");

   F1_help_topic = "brite dist";

   EFbrightdist.menuname = menu;
   EFbrightdist.menufunc = m_brite_dist;
   EFbrightdist.funcname = "brightness";
   EFbrightdist.FprevReq = 1;                                                    //  preview
   EFbrightdist.Farea = 2;                                                       //  select area usable
   EFbrightdist.Frestart = 1;                                                    //  restart allowed
   EFbrightdist.Fscript = 1;                                                     //  scripting supported 
   EFbrightdist.threadfunc = thread;
   if (! edit_setup(EFbrightdist)) return;                                       //  setup edit

/***
          __________________________________________
         |      Adjust Brightness Distribution      |
         |                                          |
         | Low Cutoff   ==========[]==============  |
         | High Cutoff  ==============[]==========  |
         | Low Flatten  =========[]===============  |
         | Mid Flatten  ============[]============  |
         | High Flatten ==============[]==========  |
         | Low Stretch  ================[]========  |
         | Mid Stretch  =============[]===========  |
         | High Stretch =========[]===============  |
         |                                          |
         |                  [Reset] [Done] [Cancel] |
         |__________________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Breset,Bdone,Bcancel,null);
   EFbrightdist.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|expand");

   zdialog_add_widget(zd,"label","labLC","vb1",E2X("Low Cutoff"));
   zdialog_add_widget(zd,"label","labHC","vb1",E2X("High Cutoff"));
   zdialog_add_widget(zd,"label","labLF","vb1",E2X("Low Flatten"));
   zdialog_add_widget(zd,"label","labMF","vb1",E2X("Mid Flatten"));
   zdialog_add_widget(zd,"label","labHF","vb1",E2X("High Flatten"));
   zdialog_add_widget(zd,"label","labLS","vb1",E2X("Low Stretch"));
   zdialog_add_widget(zd,"label","labMS","vb1",E2X("Mid Stretch"));
   zdialog_add_widget(zd,"label","labHS","vb1",E2X("High Stretch"));

   zdialog_add_widget(zd,"hscale","LC","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","HC","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","LF","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","MF","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","HF","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","LS","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","MS","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","HS","vb2","0|1.0|0.002|0","expand");

   zdialog_resize(zd,300,0);
   zdialog_run(zd,dialog_event,"save");                                          //  run dialog - parallel

   m_RGB_dist(0,0);                                                              //  popup brightness distribution
   
   LC = HC = LF = MF = HF = LS = MS = HS = 0.0;
   compute_BB();
   
   return;
}


//  dialog event and completion function

int brite_dist_names::dialog_event(zdialog *zd, cchar *event)
{
   using namespace brite_dist_names;
   
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  cancel

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();                                                           //  get full size image
      compute_BB();
      signal_thread();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         LC = HC = LF = MF = HF = LS = MS = HS = 0.0;
         zdialog_stuff(zd,"LC",LC);
         zdialog_stuff(zd,"HC",HC);
         zdialog_stuff(zd,"LF",LF);
         zdialog_stuff(zd,"MF",MF);
         zdialog_stuff(zd,"HF",HF);
         zdialog_stuff(zd,"LS",LS);
         zdialog_stuff(zd,"MS",MS);
         zdialog_stuff(zd,"HS",HS);
         compute_BB();
         edit_reset();                                                           //  19.0
      }
      else if (zd->zstat == 2 && CEF->Fmods) {                                   //  done
         edit_fullsize();                                                        //  get full size image
         compute_BB();
         signal_thread();
         m_RGB_dist(0,"kill");                                                   //  kill RGB distribution graph
         edit_done(0);                                                           //  commit edit
      }
      else {
         edit_cancel(0);                                                         //  cancel - discard edit
         m_RGB_dist(0,"kill");                                                   //  kill RGB distribution graph
      }

      return 1;                                                                  //  19.0
   }

   if (strmatch(event,"blendwidth"))                                             //  select area blendwidth change
      signal_thread();
   
   if (zstrstr("LC HC LF MF HF LS MS HS apply",event))
   {
      zdialog_fetch(zd,"LC",LC);
      zdialog_fetch(zd,"HC",HC);
      zdialog_fetch(zd,"LF",LF);
      zdialog_fetch(zd,"MF",MF);
      zdialog_fetch(zd,"HF",HF);
      zdialog_fetch(zd,"LS",LS);
      zdialog_fetch(zd,"MS",MS);
      zdialog_fetch(zd,"HS",HS);
      compute_BB();
      signal_thread();
   }

   wait_thread_idle();                                                           //  no overlap window update and threads
   Fpaintnow();
   return 1;
}


//  compute flattened brightness levels for preview size or full size image
//  FB[B] = flattened brightness level for brightness B, scaled 0-1000

void brite_dist_names::compute_BB()
{
   using namespace brite_dist_names;

   int      ii, npix, py, px, iB;
   float    *pix1;
   float    B, LC2, HC2;
   float    FB[1000];
   float    LF2, MF2, HF2, LS2, MS2, HS2;
   float    LFB, MFB, HFB, LSB, MSB, HSB;
   float    LFW, MFW, HFW, LSW, MSW, HSW, TWB;

   ww = E1pxm->ww;
   hh = E1pxm->hh;

   for (ii = 0; ii < 1000; ii++)                                                 //  clear brightness distribution data
      FB[ii] = 0;

   if (sa_stat == 3)                                                             //  process selected area
   {
      for (ii = npix = 0; ii < ww * hh; ii++)
      {
         if (! sa_pixmap[ii]) continue;
         py = ii / ww;
         px = ii - py * ww;
         pix1 = PXMpix(E1pxm,px,py);
         B = 1000.0 * (pix1[0] + pix1[1] + pix1[2]) / 768.0;
         FB[int(B)]++;
         npix++;
      }

      for (ii = 1; ii < 1000; ii++)                                              //  cumulative brightness distribution
         FB[ii] += FB[ii-1];                                                     //   0 ... npix

      for (ii = 0; ii < 1000; ii++)
         FB[ii] = FB[ii] / npix * 999.0;                                         //  flattened brightness level
   }

   else                                                                          //  process whole image
   {
      for (py = 0; py < hh; py++)                                                //  compute brightness distribution
      for (px = 0; px < ww; px++)
      {
         pix1 = PXMpix(E1pxm,px,py);
         B = 1000.0 * (pix1[0] + pix1[1] + pix1[2]) / 768.0;
         FB[int(B)]++;
      }

      for (ii = 1; ii < 1000; ii++)                                              //  cumulative brightness distribution
         FB[ii] += FB[ii-1];                                                     //   0 ... (ww * hh)
   
      for (ii = 0; ii < 1000; ii++)
         FB[ii] = FB[ii] / (ww * hh) * 999.0;                                    //  flattened brightness level
   }

   LC2 = 500 * LC;                                                               //  low cutoff, 0 ... 500
   HC2 = 1000 - 500 * HC;                                                        //  high cutoff, 1000 ... 500

   for (iB = 0; iB < 1000; iB++)                                                 //  loop brightness 0 - 1000   
   {
      B = iB;

      if (LC2 > 0 || HC2 < 1000) {                                               //  stretch to cutoff limits
         if (B < LC2) B = 0;
         else if (B > HC2) B = 999;
         else B = 1000.0 * (B - LC2) / (HC2 - LC2);
      }
      
      LF2 = LF * (1000 - B) / 1000;                                              //  low flatten  LF ... 0
      LF2 = LF2 * LF2;
      LFB = LF2 * FB[iB] + (1.0 - LF2) * B;
      
      LS2 = LS * (1000 - B) / 1000;                                              //  low stretch  LS ... 0
      LS2 = LS2 * LS2;
      LSB = B * (1 - LS2);
      
      MF2 = MF * (500 - fabsf(B - 500)) / 500;                                   //  mid flatten  0 ... MF ... 0
      MF2 = sqrtf(MF2);
      MFB = MF2 * FB[iB] + (1.0 - MF2) * B;

      MS2 = MS * (B - 500) / 500;                                                //  mid stretch  -MS ... 0 ... MS
      MSB = B * (1 + 0.5 * MS2);
      
      HF2 = HF * B / 1000;                                                       //  high flatten  0 ... HF
      HF2 = HF2 * HF2;
      HFB = HF2 * FB[iB] + (1.0 - HF2) * B;

      HS2 = HS * B / 1000;                                                       //  high stretch  0 ... HS
      HS2 = HS2 * HS2;
      HSB = B * (1 + HS2);
      
      LFW = fabsf(B - LFB) / (B + 1);                                            //  weight of each component
      LSW = fabsf(B - LSB) / (B + 1);
      MFW = fabsf(B - MFB) / (B + 1);
      MSW = fabsf(B - MSB) / (B + 1);
      HFW = fabsf(B - HFB) / (B + 1);
      HSW = fabsf(B - HSB) / (B + 1);
      
      TWB = LFW + LSW + MFW + MSW + HFW + HSW;                                   //  add weighted components
      if (TWB == 0) BB[iB] = B;
      else BB[iB] = (LFW * LFB + LSW * LSB
                   + MFW * MFB + MSW * MSB 
                   + HFW * HFB + HSW * HSB) / TWB;
   }
   
   return;
}


//  adjust brightness distribution thread function

void * brite_dist_names::thread(void *)
{
   using namespace brite_dist_names;

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

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

      do_wthreads(wthread,NWT);

      paintlock(0);                                                              //  unblock window paint               19.0

      CEF->Fmods++;                                                              //  image modified, not saved
      CEF->Fsaved = 0;
   }

   return 0;                                                                     //  not executed, stop g++ warning
}


//  worker thread for each CPU processor core

void * brite_dist_names::wthread(void *arg)
{
   using namespace brite_dist_names;

   int         index = *((int *) (arg));
   int         iB, px, py, ii, dist = 0;
   float       B, *pix1, *pix3;
   float       dold, dnew, cmax;
   float       red1, green1, blue1, red3, green3, blue3;
   
   for (py = index; py < E1pxm->hh; py += NWT)                                   //  flatten brightness distribution
   for (px = 0; px < E1pxm->ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E1pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      
      B = (pix1[0] + pix1[1] + pix1[2]) / 768.0 * 1000.0;                        //  pixel brightness scaled 0-1000
      iB = int(B);
      if (B > 0) B = BB[iB] / B;
      
      red1 = pix1[0];
      green1 = pix1[1];
      blue1 = pix1[2];
      
      red3 = B * red1;
      green3 = B * green1;
      blue3 = B * blue1;

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         dnew = sa_blendfunc(dist);                                              //    blend edge
         dold = 1.0 - dnew;
         red3 = dnew * red3 + dold * red1;
         green3 = dnew * green3 + dold * green1;
         blue3 = dnew * blue3 + dold * blue1;
      }

      cmax = red3;                                                               //  stop overflow, keep color balance
      if (green3 > cmax) cmax = green3;
      if (blue3 > cmax) cmax = blue3;
      if (cmax > 255.9) {
         cmax = 255.9 / cmax;
         red3 = red3 * cmax;
         green3 = green3 * cmax;
         blue3 = blue3 * cmax;
      }

      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

   pthread_exit(0);
}


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

   Magnify Gradients function
   enhance detail by magnifying brightness gradients

   methodology:
   get brightness gradients for each pixel in 4 directions: SE SW NE NW
   amplify gradients using the edit curve: new gradient = F(old gradient)
   integrate 4 new brightness surfaces from the amplified gradients:
     - pixel brightness = prior pixel brightness + amplified gradient
     - the Amplify control varies amplification from zero to maximum
   new pixel brightness = average from 4 calculated brightness surfaces

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

namespace gradients_names
{
   float       *brmap1, *brmap2;
   float       *brmap4[4];
   int         contrast99;
   float       amplify;
   int         ww, hh;
   editfunc    EFgradients;

   void   gradients_initz(zdialog *zd);
   int    gradients_dialog_event(zdialog *zd, cchar *event);
   void   gradients_curvedit(int);
   void * gradients_thread(void *);
   void * gradients_wthread1(void *arg);
   void * gradients_wthread2(void *arg);
   void * gradients_wthread3(void *arg);

}

//  menu function

void m_gradients(GtkWidget *, cchar *menu)
{
   using namespace gradients_names;

   F1_help_topic = "gradients";

   cchar    *title = E2X("Magnify Gradients");
   
   EFgradients.menuname = menu;
   EFgradients.menufunc = m_gradients;
   EFgradients.funcname = "gradients";
   EFgradients.Farea = 2;                                                        //  select area usable
   EFgradients.Frestart = 1;                                                     //  restart allowed
   EFgradients.FusePL = 1;                                                       //  use with paint/lever edits OK
   EFgradients.Fscript = 1;                                                      //  scripting supported
   EFgradients.threadfunc = gradients_thread;
   if (! edit_setup(EFgradients)) return;                                        //  setup: no preview, select area OK

/***
          ________________________________
         |      Magnify Gradients         |
         |  ____________________________  |
         | |                            | |
         | |                            | |
         | |    curve drawing area      | |
         | |                            | |
         | |____________________________| |
         |  low       contrast      high  |
         |                                |
         |  Amplify ========[]==========  |
         |                                |
         |  Curve File: [Open] [Save]     |
         |                                |
         |                [Done] [Cancel] |
         |________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);
   EFgradients.zd = zd;

   zdialog_add_widget(zd,"frame","frame","dialog",0,"expand");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labcL","hb1",E2X("low"),"space=4");
   zdialog_add_widget(zd,"label","labcM","hb1",Bcontrast,"expand");
   zdialog_add_widget(zd,"label","labcH","hb1",E2X("high"),"space=5");

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labcon","hb2",E2X("Amplify"),"space=5");
   zdialog_add_widget(zd,"hscale","amplify","hb2","0|1|0.005|0","expand");
   zdialog_add_widget(zd,"label","ampval","hb2",0,"space=5");

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

   GtkWidget *frame = zdialog_widget(zd,"frame");                                //  set up curve edit
   spldat *sd = splcurve_init(frame,gradients_curvedit);
   EFgradients.sd = sd;

   sd->Nspc = 1;
   sd->fact[0] = 1;
   sd->vert[0] = 0;
   sd->nap[0] = 2;                                                               //  initial curve anchor points
   sd->apx[0][0] = 0.01;
   sd->apy[0][0] = 0.10;
   sd->apx[0][1] = 0.99;
   sd->apy[0][1] = 0.10;

   splcurve_generate(sd,0);                                                      //  generate curve data

   gradients_initz(zd);                                                          //  initialize
   return;
}


//  initialization for new image file

void gradients_names::gradients_initz(zdialog *zd)
{
   using namespace gradients_names;

   int         ii, cc, px, py;
   float       b1, *pix1;
   int         jj, sum, limit, condist[100];

   ww = E1pxm->ww;                                                               //  image dimensions
   hh = E1pxm->hh;

   cc = ww * hh * sizeof(float);                                                 //  allocate brightness map memory
   brmap1 = (float *) zmalloc(cc);
   brmap2 = (float *) zmalloc(cc);
   for (ii = 0; ii < 4; ii++)
      brmap4[ii] = (float *) zmalloc(cc);

   for (py = 0; py < hh; py++)                                                   //  map initial image brightness
   for (px = 0; px < ww; px++)
   {
      ii = py * ww + px;
      pix1 = PXMpix(E1pxm,px,py);
      b1 = 0.333 * (pix1[0] + pix1[1] + pix1[2]);                                //  pixel brightness 0-255.9
      if (b1 < 1) b1 = 1;
      brmap1[ii] = b1;
   }

   for (ii = 0; ii < 100; ii++)
      condist[ii] = 0;

   for (py = 1; py < hh; py++)                                                   //  map contrast distribution
   for (px = 1; px < ww; px++)
   {
      ii = py * ww + px;
      jj = 0.388 * fabsf(brmap1[ii] - brmap1[ii-1]);                             //  horiz. contrast, ranged 0 - 99
      if (jj > 99) jj = 99;                                                      //  trap bad image data
      condist[jj]++;
      jj = 0.388 * fabsf(brmap1[ii] - brmap1[ii-ww]);                            //  vertical
      if (jj > 99) jj = 99;
      condist[jj]++;
   }

   sum = 0;
   limit = 0.99 * 2 * (ww-1) * (hh-1);                                           //  find 99th percentile contrast

   for (ii = 0; ii < 100; ii++) {
      sum += condist[ii];
      if (sum > limit) break;
   }

   contrast99 = 255.0 * ii / 100.0;                                              //  0 to 255
   if (contrast99 < 4) contrast99 = 4;                                           //  rescale low-contrast image

   zdialog_resize(zd,250,300);
   zdialog_run(zd,gradients_dialog_event,"save");                                //  run dialog - parallel

   amplify = 0;
   return;
}


//  dialog event and completion callback function

int gradients_names::gradients_dialog_event(zdialog *zd, cchar *event)
{
   using namespace gradients_names;

   spldat   *sd = EFgradients.sd;    
   char     text[8];

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  cancel

   if (zd->zstat) {                                                              //  dialog complete
      if (zd->zstat == 1) edit_done(0);
      else edit_cancel(0);
      zfree(brmap1);                                                             //  free memory
      zfree(brmap2);
      brmap1 = brmap2 = 0;
      for (int ii = 0; ii < 4; ii++) {
         zfree(brmap4[ii]);
         brmap4[ii] = 0;
      }
      return 1;
   }
   
   if (strmatch(event,"apply"))                                                  //  from script
      gradients_dialog_event(zd,"amplify");

   if (strmatch(event,"blendwidth")) signal_thread();

   if (strmatch(event,"amplify")) {                                              //  slider value
      zdialog_fetch(zd,"amplify",amplify);
      snprintf(text,8,"%.2f",amplify);                                           //  numeric feedback
      zdialog_stuff(zd,"ampval",text);
      signal_thread();                                                           //  trigger update thread
   }

   if (strmatch(event,"loadcurve")) {                                            //  load saved curve
      splcurve_load(sd);
      signal_thread();
      return 0;
   }

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

   return 0;
}


//  this function is called when the curve is edited

void gradients_names::gradients_curvedit(int)
{
   signal_thread();
   return;
}


//  thread function

void * gradients_names::gradients_thread(void *)
{
   using namespace gradients_names;

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

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

      do_wthreads(gradients_wthread1,4);
      do_wthreads(gradients_wthread2,4);
      do_wthreads(gradients_wthread3,NWT);

      CEF->Fmods++;
      CEF->Fsaved = 0;
      if (amplify == 0) CEF->Fmods = 0;

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

   return 0;                                                                     //  not executed, stop g++ warning
}


//  working threads

void * gradients_names::gradients_wthread1(void *arg)
{
   using namespace gradients_names;

   int         ii, kk, bii, pii, dist = 0;
   int         px, px1, px2, pxinc;
   int         py, py1, py2, pyinc;
   float       b1, b4, xval, yval, grad;
   float       amp, con99;
   spldat      *sd = EFgradients.sd;    

   con99 = contrast99;                                                           //  99th percentile contrast
   con99 = 1.0 / con99;                                                          //  inverted

   amp = pow(amplify,0.5);                                                       //  get amplification, 0 to 1

   bii = *((int *) arg);                                                         //  thread 0...3

   if (bii == 0) {                                                               //  direction SE
      px1 = 1; px2 = ww; pxinc = 1;
      py1 = 1; py2 = hh; pyinc = 1;
      pii = - 1 - ww;
   }

   else if (bii == 1) {                                                          //  direction SW
      px1 = ww-2; px2 = 0; pxinc = -1;
      py1 = 1; py2 = hh; pyinc = 1;
      pii = + 1 - ww;
   }

   else if (bii == 2) {                                                          //  direction NE
      px1 = 1; px2 = ww; pxinc = 1;
      py1 = hh-2; py2 = 0; pyinc = -1;
      pii = - 1 + ww;
   }

   else {   /* bii == 3 */                                                       //  direction NW
      px1 = ww-2; px2 = 0; pxinc = -1;
      py1 = hh-2; py2 = 0; pyinc = -1;
      pii = + 1 + ww;
   }

   for (ii = 0; ii < ww * hh; ii++)                                              //  initial brightness map
      brmap4[bii][ii] = brmap1[ii];

   for (py = py1; py != py2; py += pyinc)                                        //  loop all image pixels
   for (px = px1; px != px2; px += pxinc)
   {
      ii = py * ww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      b1 = brmap1[ii];                                                           //  this pixel brightness
      grad = b1 - brmap1[ii+pii];                                                //   - prior pixel --> gradient

      xval = fabsf(grad) * con99;                                                //  gradient scaled 0 to 1+
      kk = 1000.0 * xval;
      if (kk > 999) kk = 999;
      yval = 1.0 + 5.0 * sd->yval[0][kk];                                        //  amplifier = 1...6
      grad = grad * yval;                                                        //  magnified gradient

      b4 = brmap4[bii][ii+pii] + grad;                                           //  pixel brightness = prior + gradient
      b4 = (1.0 - amp) * b1 + amp * b4;                                          //  apply amplifier: b4 range = b1 --> b4

      brmap4[bii][ii] = b4;                                                      //  new pixel brightness
   }

   pthread_exit(0);
}


void * gradients_names::gradients_wthread2(void *arg)
{
   using namespace gradients_names;

   int      index, ii, px, py, dist = 0;
   float    b4;

   index = *((int *) arg);                                                       //  thread 0...3

   for (py = index; py < hh; py += 4)                                            //  loop all image pixels
   for (px = 0; px < ww; px++)
   {
      ii = py * ww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel is outside area
      }

      b4 = brmap4[0][ii] + brmap4[1][ii]                                         //  new brightness = average of four
         + brmap4[2][ii] + brmap4[3][ii];                                        //    calculated brightness surfaces
      b4 = 0.25 * b4;

      if (b4 < 1) b4 = 1;

      brmap2[ii] = b4;
   }

   pthread_exit(0);
}


void * gradients_names::gradients_wthread3(void *arg)
{
   using namespace gradients_names;

   float       *pix1, *pix3;
   int         index, ii, px, py, dist = 0;
   float       b1, b2, bf, f1, f2;
   float       red1, green1, blue1, red3, green3, blue3;

   index = *((int *) arg);

   for (py = index; py < hh; py += NWT)                                          //  loop all image pixels
   for (px = 0; px < ww; px++)
   {
      ii = py * ww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel is outside area
      }

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

      b1 = brmap1[ii];                                                           //  initial pixel brightness
      b2 = brmap2[ii];                                                           //  calculated new brightness

      bf = b2 / b1;                                                              //  brightness ratio

      red1 = pix1[0];                                                            //  input RGB
      green1 = pix1[1];
      blue1 = pix1[2];

      red3 = bf * red1;                                                          //  output RGB
      if (red3 > 255) red3 = 255;
      green3 = bf * green1;
      if (green3 > 255) green3 = 255;
      blue3 = bf * blue1;
      if (blue3 > 255) blue3 = 255;

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes
         f2 = 1.0 - f1;
         red3 = f1 * red3 + f2 * red1;
         green3 = f1 * green3 + f2 * green1;
         blue3 = f1 * blue3 + f2 * blue1;
      }

      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

   pthread_exit(0);
}


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

//  flatten brightness distribution based on the distribution of nearby zones

namespace flatten_names
{
   int      Zinit;                                 //  zone initialization needed
   float    flatten;                               //  flatten amount, 0 - 100
   float    deband1, deband2;                      //  deband dark, bright, 0 - 100
   int      Iww, Ihh;                              //  image dimensions
   int      NZ;                                    //  no. of image zones
   int      pNZ;                                   //  prior image zones
   int      Zsize, Zrows, Zcols;                   //  zone parameters
   float    *Br;                                   //  Br[py][px]  pixel brightness
   int      *Zxlo, *Zylo, *Zxhi, *Zyhi;            //  Zxlo[ii] etc.  zone ii pixel range
   int      *Zcen;                                 //  Zcen[ii][2]  zone ii center (py,px)
   int16    *Zn;                                   //  Zn[py][px][9]  9 nearest zones for pixel: 0-200 (-1 = none)
   uint8    *Zw;                                   //  Zw[py][px][9]  zone weights for pixel: 0-100 = 1.0
   float    *Zff;                                  //  Zff[ii][1000]  zone ii flatten factors

   editfunc    EFflatten;

   int    dialog_event(zdialog* zd, cchar *event);
   void   doflatten();
   void   calczones();
   void   initzones();
   void * thread(void *);
   void * wthread(void *);
}


//  menu function

void m_flatten(GtkWidget *, cchar *menu)
{
   using namespace flatten_names;

   cchar  *title = E2X("Flatten Brightness");

   F1_help_topic = "flatten";

   Iww = Fpxb->ww;
   Ihh = Fpxb->hh;

   if (Iww * Ihh > wwhh_limit2) {
      zmessageACK(Mwin,"image too big");
      edit_cancel(0);
      return;
   }

   EFflatten.menuname = menu;
   EFflatten.menufunc = m_flatten;
   EFflatten.funcname = "flatten";
   EFflatten.FprevReq = 1;                                                       //  use preview image
   EFflatten.Farea = 2;                                                          //  select area usable
   EFflatten.Frestart = 1;                                                       //  restartable
   EFflatten.FusePL = 1;                                                         //  use with paint/lever edits OK
   EFflatten.Fscript = 1;                                                        //  scripting supported
   EFflatten.threadfunc = thread;
   if (! edit_setup(EFflatten)) return;                                          //  setup edit

   Iww = E0pxm->ww;
   Ihh = E0pxm->hh;

   if (Iww * Ihh > wwhh_limit2) {
      zmessageACK(Mwin,"image too big");
      edit_cancel(0);
      return;
   }

/***
          ______________________________________
         |        Flatten Brightness            |
         |                                      |
         | Zones  [ 123 ]   [Apply]          |
         | Flatten  =========[]============ NN  |
         | Deband Dark =========[]========= NN  |
         | Deband Bright ==========[]====== NN  |
         |                                      |
         |                     [Done] [Cancel]  |
         |______________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);
   EFflatten.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labreg","hb1",E2X("Zones"),"space=5");
   zdialog_add_widget(zd,"zspin","zones","hb1","1|999|1|100");                   //  zone range 1-999
   zdialog_add_widget(zd,"button","apply","hb1",Bapply,"space=10");
   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"label","labflatten","hb2",Bflatten,"space=5");
   zdialog_add_widget(zd,"hscale","flatten","hb2","0|100|1|0","expand");
   zdialog_add_widget(zd,"hbox","hb3","dialog");
   zdialog_add_widget(zd,"label","labdeband1","hb3",E2X("Deband Dark"),"space=5");
   zdialog_add_widget(zd,"hscale","deband1","hb3","0|100|1|0","expand");
   zdialog_add_widget(zd,"hbox","hb4","dialog");
   zdialog_add_widget(zd,"label","labdeband2","hb4",E2X("Deband Bright"),"space=5");
   zdialog_add_widget(zd,"hscale","deband2","hb4","0|100|1|0","expand");

   zdialog_resize(zd,300,0);
   zdialog_run(zd,dialog_event,"save");                                          //  run dialog - parallel

   NZ = pNZ = 40;                                                                //  default zone count
   calczones();                                                                  //  adjust to fit image
   flatten = deband1 = deband2 = 0;                                              //  dialog controls = neutral
   Zinit = 1;                                                                    //  zone initialization needed
   Br = 0;                                                                       //  no memory allocated
   return;
}


//  dialog event and completion function

int flatten_names::dialog_event(zdialog *zd, cchar *event)
{
   using namespace flatten_names;
   
   if (strmatch(event,"done")) zd->zstat = 1;                                    //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  cancel

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      Zinit = 1;
      doflatten();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         edit_fullsize();                                                        //  get full size image
         Zinit = 1;                                                              //  recalculate zones
         doflatten();                                                            //  flatten full image
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit

      if (Br) {
         zfree(Br);                                                              //  free memory
         zfree(Zn);
         zfree(Zw);
         zfree(Zxlo);
         zfree(Zylo);
         zfree(Zxhi);
         zfree(Zyhi);
         zfree(Zcen);
         zfree(Zff);
         Br = 0;
      }

      return 1;
   }

   if (strmatch(event,"apply")) {                                                //  [apply]  (also from script)
      dialog_event(zd,"zones");
      dialog_event(zd,"flatten");
      dialog_event(zd,"deband1");
      dialog_event(zd,"deband2");
      doflatten();
   }

   if (strmatch(event,"blendwidth"))                                             //  select area blendwidth change
      doflatten();

   if (strmatch(event,"zones")) {                                                //  get zone count input
      zdialog_fetch(zd,"zones",NZ);
      if (NZ == pNZ) return 1;
      calczones();                                                               //  adjust to fit image
      zdialog_stuff(zd,"zones",NZ);                                              //  update dialog
      Zinit = 1;                                                                 //  zone initialization needed
   }

   if (strmatch(event,"flatten")) {
      zdialog_fetch(zd,"flatten",flatten);                                       //  get slider values
      doflatten();
   }

   if (strmatch(event,"deband1")) {
      zdialog_fetch(zd,"deband1",deband1);
      doflatten();
   }

   if (strmatch(event,"deband2")) {
      zdialog_fetch(zd,"deband2",deband2);
      doflatten();
   }

   return 1;
}


//  perform the flatten calculations and update the image

void flatten_names::doflatten()
{
   if (flatten > 0) {
      signal_thread();
      wait_thread_idle();                                                        //  no overlap window update and threads
      Fpaintnow();
   }
   else edit_reset();
   return;
}


//  recalculate zone count based on what fits the image dimensions
//  done only when the user zone count input changes
//  outputs: NZ, Zrows, Zcols

void flatten_names::calczones()
{
   int      gNZ, dNZ;

   Iww = E1pxm->ww;                                                              //  image dimensions
   Ihh = E1pxm->hh;

   gNZ = NZ;                                                                     //  new zone count goal
   dNZ = NZ - pNZ;                                                               //  direction of change

   while (true)
   {
      Zsize = sqrtf(Iww * Ihh / gNZ);                                            //  approx. zone size
      Zrows = Ihh / Zsize + 0.5;                                                 //  get appropriate rows and cols
      Zcols = Iww / Zsize + 0.5;
      if (Zrows < 1) Zrows = 1;
      if (Zcols < 1) Zcols = 1;
      NZ = Zrows * Zcols;                                                        //  NZ matching rows x cols

      if (dNZ > 0 && NZ <= pNZ) {                                                //  no increase, try again
         if (NZ >= 999) break;
         gNZ++;
         continue;
      }

      if (dNZ < 0 && NZ >= pNZ) {                                                //  no decrease, try again
         if (NZ <= 20) break;
         gNZ--;
         continue;
      }

      if (dNZ == 0) break;
      if (dNZ > 0 && NZ > pNZ) break;
      if (dNZ < 0 && NZ < pNZ) break;
   }

   pNZ = NZ;                                                                     //  final zone count
   dNZ = 0;
   return;
}


//  build up the zones data when NZ or the image size changes
//  (preview or full size image)

void flatten_names::initzones()
{
   int      px, py, cx, cy;
   int      rx, ii, jj, kk;
   int      zww, zhh, row, col;
   float    *pix1, bright;
   float    weight[9], sumweight;
   float    D, Dthresh;

   if (Br) {
      zfree(Br);                                                                 //  free memory
      zfree(Zn);
      zfree(Zw);
      zfree(Zxlo);
      zfree(Zylo);
      zfree(Zxhi);
      zfree(Zyhi);
      zfree(Zcen);
      zfree(Zff);
      Br = 0;
   }

   Iww = E1pxm->ww;                                                              //  image dimensions
   Ihh = E1pxm->hh;

   Br = (float *) zmalloc(Iww * Ihh * sizeof(float));                            //  allocate memory
   Zn = (int16 *) zmalloc(Iww * Ihh * 9 * sizeof(int16));
   Zw = (uint8 *) zmalloc(Iww * Ihh * 9 * sizeof(uint8));
   Zxlo = (int *) zmalloc(NZ * sizeof(int));
   Zylo = (int *) zmalloc(NZ * sizeof(int));
   Zxhi = (int *) zmalloc(NZ * sizeof(int));
   Zyhi = (int *) zmalloc(NZ * sizeof(int));
   Zcen = (int *) zmalloc(NZ * 2 * sizeof(int));
   Zff = (float *) zmalloc(NZ * 1000 * sizeof(float));

   for (py = 0; py < Ihh; py++)                                                  //  get initial pixel brightness levels
   for (px = 0; px < Iww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);
      bright = pixbright(pix1);
      ii = py * Iww + px;
      Br[ii] = bright;
   }

   zww = Iww / Zcols;                                                            //  actual zone size
   zhh = Ihh / Zrows;

   for (row = 0; row < Zrows; row++)
   for (col = 0; col < Zcols; col++)                                             //  set low/high bounds per zone
   {
      ii = row * Zcols + col;
      Zxlo[ii] = col * zww;
      Zylo[ii] = row * zhh;
      Zxhi[ii] = Zxlo[ii] + zww;
      Zyhi[ii] = Zylo[ii] + zhh;
      Zcen[2*ii] = Zylo[ii] + zhh/2;
      Zcen[2*ii+1] = Zxlo[ii] + zww/2;
   }

   for (ii = 0; ii < NZ; ii++)                                                   //  compute brightness distributiion
   {                                                                             //    for each zone
      for (jj = 0; jj < 1000; jj++)
         Zff[1000*ii+jj] = 0;

      for (py = Zylo[ii]; py < Zyhi[ii]; py++)                                   //  brightness distribution
      for (px = Zxlo[ii]; px < Zxhi[ii]; px++)
      {
         pix1 = PXMpix(E1pxm,px,py);
         kk = pixbright(pix1);
         if (kk > 255) kk = 255;
         bright = 3.906 * kk;                                                    //  1000 / 256 * kk
         Zff[1000*ii+int(bright)]++;
      }

      for (jj = 1; jj < 1000; jj++)                                              //  cumulative brightness distribution
         Zff[1000*ii+jj] += Zff[1000*ii+jj-1];

      for (jj = 0; jj < 1000; jj++)                                              //  multiplier per brightness level
         Zff[1000*ii+jj] = Zff[1000*ii+jj] / (zww*zhh) * 1000 / (jj+1);          //    to make distribution flat
   }

   for (py = 0; py < Ihh; py++)                                                  //  set 9 nearest zones per pixel
   for (px = 0; px < Iww; px++)
   {
      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      row = py / zhh;                                                            //  zone containing pixel
      col = px / zww;

      ii = 0;
      for (jj = row-1; jj <= row+1; jj++)                                        //  loop 3x3 surrounding zones
      for (kk = col-1; kk <= col+1; kk++)
      {
         if (jj >= 0 && jj < Zrows && kk >= 0 && kk < Zcols)                     //  zone is not off the edge
            Zn[rx+ii] = jj * Zcols + kk;
         else Zn[rx+ii] = -1;                                                    //  edge row/col: missing neighbor
         ii++;
      }
   }

   if (zww < zhh)                                                                //  pixel to zone distance threshold
      Dthresh = 1.5 * zww;                                                       //  area influence = 0 beyond this distance
   else Dthresh = 1.5 * zhh;

   for (py = 0; py < Ihh; py++)                                                  //  set zone weights per pixel
   for (px = 0; px < Iww; px++)
   {
      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      for (ii = 0; ii < 9; ii++)                                                 //  distance to each zone center
      {
         jj = Zn[rx+ii];
         if (jj == -1) {                                                         //  missign zone
            weight[ii] = 0;                                                      //  weight = 0
            continue;
         }
         cy = Zcen[2*jj];
         cx = Zcen[2*jj+1];
         D = sqrtf((py-cy)*(py-cy) + (px-cx)*(px-cx));                           //  distance from pixel to zone center
         D = D / Dthresh;
         if (D > 1) D = 1;                                                       //  zone influence reaches zero at Dthresh
         weight[ii] = 1.0 - D;
      }

      sumweight = 0;
      for (ii = 0; ii < 9; ii++)                                                 //  zone weights based on distance
         sumweight += weight[ii];

      for (ii = 0; ii < 9; ii++)                                                 //  normalize weights, sum = 1.0
         Zw[rx+ii] = 100 * weight[ii] / sumweight;                               //  0-100 = 1.0
   }

   return;
}


//  adjust brightness distribution thread function

void * flatten_names::thread(void *)
{
   using namespace flatten_names;

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

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

      if (Zinit) initzones();                                                    //  reinitialize zones
      Zinit = 0;

      do_wthreads(wthread,NWT);

      paintlock(0);                                                              //  unblock window paint               19.0

      if (! flatten) CEF->Fmods = 0;                                             //  no modification
      else {
         CEF->Fmods++;                                                           //  image modified, not saved
         CEF->Fsaved = 0;
      }
   }

   return 0;                                                                     //  not executed, stop g++ warning
}


//  worker thread for each CPU processor core

void * flatten_names::wthread(void *arg)
{
   using namespace flatten_names;

   int         index = *((int *) (arg));
   int         px, py, rx, ii, jj, dist = 0;
   float       *pix1, *pix3;
   float       fnew1, fnew2, fnew, fold;
   float       dold, dnew, cmax;
   float       red1, green1, blue1, red3, green3, blue3;
   float       FF, weight, bright;

   for (py = index; py < Ihh; py += NWT)                                         //  loop all image pixels
   for (px = 0; px < Iww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E1pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

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

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

      bright = 0.976 * red1 + 2.539 * green1 + 0.39 * blue1;                     //  brightness scaled 0-999.9
      
      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      FF = 0;
      for (ii = 0; ii < 9; ii++) {                                               //  loop 9 nearest zones
         weight = Zw[rx+ii];
         if (weight > 0) {                                                       //  0-100 = 1.0
            jj = Zn[rx+ii];
            FF += 0.01 * weight * Zff[1000*jj+int(bright)];                      //  sum weight * flatten factor
         }
      }

      red3 = FF * red1;                                                          //  fully flattened brightness
      green3 = FF * green1;
      blue3 = FF * blue1;
      
      fnew1 = 1 - 0.01 * deband1 * 0.001 * (1000 - bright);                      //  attenuate dark pixels
      fnew2 = 1 - 0.01 * deband2 * 0.001 * bright;                               //  attenuate bright pixels
      fnew = fnew1 * fnew2;

      fnew = fnew * 0.01 * flatten;                                              //  how much to flatten, 0 to 1
      fold = 1.0 - fnew;                                                         //  how much to retain, 1 to 0

      red3 = fnew * red3 + fold * red1;                                          //  blend new and old brightness
      green3 = fnew * green3 + fold * green1;
      blue3 = fnew * blue3 + fold * blue1;

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         dnew = sa_blendfunc(dist);                                              //  blend changes over sa_blendwidth
         dold = 1.0 - dnew;
         red3 = dnew * red3 + dold * red1;
         green3 = dnew * green3 + dold * green1;
         blue3 = dnew * blue3 + dold * blue1;
      }

      cmax = red3;                                                               //  stop overflow, keep color balance
      if (green3 > cmax) cmax = green3;
      if (blue3 > cmax) cmax = blue3;
      if (cmax > 255.9) {
         cmax = 255.9 / cmax;
         red3 = red3 * cmax;
         green3 = green3 * cmax;
         blue3 = blue3 * cmax;
      }
      
      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

   pthread_exit(0);
}


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

//  Global Retinex function
//  Rescale RGB values based on entire image - eliminate color caste and reduce fog/haze.

namespace gretinex_names 
{
   editfunc    EFgretinex;                                                       //  edit function data
   int         e3ww, e3hh;
   cchar       *thread_command;

   float       Rdark, Gdark, Bdark;
   float       Rbrite, Gbrite, Bbrite;
   float       Rmpy, Gmpy, Bmpy;
   float       pRdark, pGdark, pBdark;                                           //  prior values 
   float       pRbrite, pGbrite, pBbrite;
   float       pRmpy, pGmpy, pBmpy;
   float       blend, reducebright;
   int         Fbrightpoint, Fdarkpoint;
}


//  menu function

void m_gretinex(GtkWidget *, cchar *menu)                                        //  separated from zonal retinex
{
   using namespace gretinex_names;

   int    gretinex_dialog_event(zdialog *zd, cchar *event);
   void   gretinex_mousefunc();
   void * gretinex_thread(void *);

   F1_help_topic = "global retx";

   EFgretinex.menuname = menu;
   EFgretinex.menufunc = m_gretinex;
   EFgretinex.funcname = "Gretinex";                                             //  function name
   EFgretinex.Farea = 2;                                                         //  select area usable
   EFgretinex.Frestart = 1;                                                      //  allow restart
   EFgretinex.Fscript = 1;                                                       //  scripting supported
   EFgretinex.threadfunc = gretinex_thread;                                      //  thread function
   EFgretinex.mousefunc = gretinex_mousefunc;                                    //  mouse function

   if (! edit_setup(EFgretinex)) return;                                         //  setup edit

   e3ww = E3pxm->ww;                                                             //  image size
   e3hh = E3pxm->hh;
   E9pxm = 0;
   
/***
          ______________________________________
         |           Global Retinex             |
         |                                      |
         |               Red  Green  Blue   +/- |
         |  Dark Point:  [__]  [__]  [__]  [__] |        0 ... 255      neutral point is 0
         | Bright Point: [__]  [__]  [__]  [__] |        0 ... 255      neutral point is 255
         |  Multiplyer:  [__]  [__]  [__]  [__] |        0 ... 5        neutral point is 1
         |                                      |
         | [auto] brightness rescale            |
         | [x] bright point  [x] dark point     |        mouse click spots
         | blend: =======[]===================  |
         | reduce bright: ==========[]========  |
         |                                      |
         |             [reset] [done] [cancel]  |
         |______________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Global Retinex"),Mwin,Breset,Bdone,Bcancel,null); 
   EFgretinex.zd = zd;
   
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","space","hb1","","space=2");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog");
   zdialog_add_widget(zd,"label","space","hb1","","space=2");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog");
   zdialog_add_widget(zd,"label","space","hb1","","space=2");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"homog");
   zdialog_add_widget(zd,"label","space","hb1","","space=2");
   zdialog_add_widget(zd,"vbox","vb4","hb1",0,"homog");
   zdialog_add_widget(zd,"label","space","hb1","","space=2");
   zdialog_add_widget(zd,"vbox","vb5","hb1",0,"homog");
   
   zdialog_add_widget(zd,"label","space","vb1","");
   zdialog_add_widget(zd,"label","labdark","vb1",E2X("Dark Point"));
   zdialog_add_widget(zd,"label","labbrite","vb1",E2X("Bright Point"));
   zdialog_add_widget(zd,"label","labmpy","vb1",E2X("Multiplyer"));

   zdialog_add_widget(zd,"label","labred","vb2",Bred);
   zdialog_add_widget(zd,"zspin","Rdark","vb2","0|255|1|0","size=3");
   zdialog_add_widget(zd,"zspin","Rbrite","vb2","0|255|1|255","size=3");
   zdialog_add_widget(zd,"zspin","Rmpy","vb2","0.1|5|0.01|1","size=3");

   zdialog_add_widget(zd,"label","labgreen","vb3",Bgreen);
   zdialog_add_widget(zd,"zspin","Gdark","vb3","0|255|1|0","size=3");
   zdialog_add_widget(zd,"zspin","Gbrite","vb3","0|255|1|255","size=3");
   zdialog_add_widget(zd,"zspin","Gmpy","vb3","0.1|5|0.01|1","size=3");

   zdialog_add_widget(zd,"label","labred","vb4",Bblue);
   zdialog_add_widget(zd,"zspin","Bdark","vb4","0|255|1|0","size=3");
   zdialog_add_widget(zd,"zspin","Bbrite","vb4","0|255|1|255","size=3");
   zdialog_add_widget(zd,"zspin","Bmpy","vb4","0.1|5|0.01|1","size=3");

   zdialog_add_widget(zd,"label","laball","vb5",Ball);
   zdialog_add_widget(zd,"zspin","dark+-","vb5","-1|+1|1|0","size=3");
   zdialog_add_widget(zd,"zspin","brite+-","vb5","-1|+1|1|0","size=3");
   zdialog_add_widget(zd,"zspin","mpy+-","vb5","-1|+1|1|0","size=3");

   zdialog_add_widget(zd,"hbox","hbauto","dialog");
   zdialog_add_widget(zd,"button","autoscale","hbauto",Bauto,"space=3");
   zdialog_add_widget(zd,"label","labauto","hbauto",E2X("brightness rescale"),"space=5");

   zdialog_add_widget(zd,"hbox","hbclicks","dialog");
   zdialog_add_widget(zd,"check","brightpoint","hbclicks",E2X("click bright point"),"space=3");
   zdialog_add_widget(zd,"check","darkpoint","hbclicks",E2X("click dark point"),"space=5");
   
   zdialog_add_widget(zd,"hbox","hbb","dialog");
   zdialog_add_widget(zd,"label","labb","hbb",E2X("blend"),"space=5");
   zdialog_add_widget(zd,"hscale","blend","hbb","0|1.0|0.01|1.0","expand");
   zdialog_add_widget(zd,"hbox","hbrd","dialog");
   zdialog_add_widget(zd,"label","labrd","hbrd",E2X("reduce bright"),"space=5");
   zdialog_add_widget(zd,"hscale","reduce bright","hbrd","0|1.0|0.01|0.0","expand");

   zdialog_run(zd,gretinex_dialog_event,"save");                                 //  run dialog - parallel
   zdialog_send_event(zd,"reset");
   return;
}


//  dialog event and completion function

int gretinex_dialog_event(zdialog *zd, cchar *event)
{
   using namespace gretinex_names;
   
   void  gretinex_mousefunc();

   int      adddark, addbrite, addmpy;
   int      Fchange = 0;
   
   if (strmatch(event,"focus")) return 1;                                        //  stop loss of button event (?)      19.0
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"reset")) zd->zstat = 1;                                   //  initz. 
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         Rdark = Gdark = Bdark = 0;                                              //  set neutral values
         Rbrite = Gbrite = Bbrite = 255;
         pRdark = pGdark = pBdark = 0;                                           //  prior values = same
         pRbrite = pGbrite = pBbrite = 255;                                      //  (no change)
         Rmpy = Gmpy = Bmpy = 1.0;
         Fbrightpoint = Fdarkpoint = 0;
         blend = 1.0;
         reducebright = 0.0;

         zdialog_stuff(zd,"Rdark",0);                                            //  stuff neutral values into dialog
         zdialog_stuff(zd,"Gdark",0);
         zdialog_stuff(zd,"Bdark",0);
         zdialog_stuff(zd,"Rbrite",255);
         zdialog_stuff(zd,"Gbrite",255);
         zdialog_stuff(zd,"Bbrite",255);
         zdialog_stuff(zd,"Rmpy",1.0);
         zdialog_stuff(zd,"Gmpy",1.0);
         zdialog_stuff(zd,"Bmpy",1.0);
         zdialog_stuff(zd,"brightpoint",0);
         zdialog_stuff(zd,"darkpoint",0);

         edit_reset();
         if (E9pxm) PXM_free(E9pxm);                                             //  free memory
         E9pxm = 0;
         return 1;
      }

      else if (zd->zstat == 2) {                                                 //  done
         edit_done(0);                                                           //  commit edit
         if (E9pxm) PXM_free(E9pxm);                                             //  free memory
         E9pxm = 0;
         return 1;
      }

      else {
         edit_cancel(0);                                                         //  discard edit
         if (E9pxm) PXM_free(E9pxm);                                             //  free memory
         E9pxm = 0;
         return 1;
      }
   }

   Fbrightpoint = Fdarkpoint = 0;                                                //  disable mouse

   if (strmatch(event,"autoscale"))                                              //  auto set dark and bright points
   {
      edit_reset();

      thread_command = "autoscale";                                              //  get dark/bright limits from
      signal_thread();                                                           //    darkest/brightest pixels
      wait_thread_idle();

      zdialog_stuff(zd,"Rdark",Rdark);                                           //  update dialog widgets
      zdialog_stuff(zd,"Gdark",Gdark);
      zdialog_stuff(zd,"Bdark",Bdark);
      zdialog_stuff(zd,"Rbrite",Rbrite);
      zdialog_stuff(zd,"Gbrite",Gbrite);
      zdialog_stuff(zd,"Bbrite",Bbrite);
      zdialog_stuff(zd,"Rmpy",1.0);
      zdialog_stuff(zd,"Gmpy",1.0);
      zdialog_stuff(zd,"Bmpy",1.0);
      zdialog_stuff(zd,"brightpoint",0);
      zdialog_stuff(zd,"darkpoint",0);

      pRdark = Rdark;                                                            //  prior values = same
      pGdark = Gdark;
      pBdark = Bdark;
      pRbrite = Rbrite; 
      pGbrite = Gbrite; 
      pBbrite = Bbrite;
      pRmpy = Rmpy;
      pGmpy = Gmpy;
      pBmpy = Bmpy;
      return 1;
   }
   
   if (strmatch(event,"brightpoint")) {
      zdialog_fetch(zd,"brightpoint",Fbrightpoint);
      if (Fbrightpoint) {
         zdialog_stuff(zd,"darkpoint",0);
         Fdarkpoint = 0;
      }
   }

   if (strmatch(event,"darkpoint")) {
      zdialog_fetch(zd,"darkpoint",Fdarkpoint);
      if (Fdarkpoint) {
         zdialog_stuff(zd,"brightpoint",0);
         Fbrightpoint = 0;
      }
   }
   
   if (zstrstr("brightpoint darkpoint",event)) {                                 //  brightpoint or darkpoint
      takeMouse(gretinex_mousefunc,dragcursor);                                  //     connect mouse function
      return 1;
   }
   else {
      zdialog_stuff(zd,"brightpoint",0);                                         //  reset zdialog checkboxes
      zdialog_stuff(zd,"darkpoint",0);
   }
   
   adddark = addbrite = addmpy = 0;
   if (strmatch(event,"dark+-")) zdialog_fetch(zd,"dark+-",adddark);             //  [+/-] button
   if (strmatch(event,"brite+-")) zdialog_fetch(zd,"brite+-",addbrite);
   if (strmatch(event,"mpy+-")) zdialog_fetch(zd,"mpy+-",addmpy);
   
   if (adddark) {
      zdialog_fetch(zd,"Rdark",Rdark);                                           //  increment dark levels
      zdialog_fetch(zd,"Gdark",Gdark);
      zdialog_fetch(zd,"Bdark",Bdark);
      Rdark += adddark;
      Gdark += adddark;
      Bdark += adddark;
      if (Rdark < 0) Rdark = 0;
      if (Gdark < 0) Gdark = 0;
      if (Bdark < 0) Bdark = 0;
      if (Rdark > 255) Rdark = 255;
      if (Gdark > 255) Gdark = 255;
      if (Bdark > 255) Bdark = 255;
      zdialog_stuff(zd,"Rdark",Rdark);
      zdialog_stuff(zd,"Gdark",Gdark);
      zdialog_stuff(zd,"Bdark",Bdark);
   }

   if (addbrite) {                                                               //  increment bright levels
      zdialog_fetch(zd,"Rbrite",Rbrite);
      zdialog_fetch(zd,"Gbrite",Gbrite);
      zdialog_fetch(zd,"Bbrite",Bbrite);
      Rbrite += addbrite;
      Gbrite += addbrite;
      Bbrite += addbrite;
      if (Rbrite < 0) Rbrite = 0;
      if (Gbrite < 0) Gbrite = 0;
      if (Bbrite < 0) Bbrite = 0;
      if (Rbrite > 255) Rbrite = 255;
      if (Gbrite > 255) Gbrite = 255;
      if (Bbrite > 255) Bbrite = 255;
      zdialog_stuff(zd,"Rbrite",Rbrite);
      zdialog_stuff(zd,"Gbrite",Gbrite);
      zdialog_stuff(zd,"Bbrite",Bbrite);
   }

   if (addmpy) {                                                                 //  increment mpy factors
      zdialog_fetch(zd,"Rmpy",Rmpy);
      zdialog_fetch(zd,"Gmpy",Gmpy);
      zdialog_fetch(zd,"Bmpy",Bmpy);
      Rmpy += 0.01 * addmpy;
      Gmpy += 0.01 * addmpy;
      Bmpy += 0.01 * addmpy;
      if (Rmpy < 0.1) Rmpy = 0.1;
      if (Gmpy < 0.1) Gmpy = 0.1;
      if (Bmpy < 0.1) Bmpy = 0.1;
      if (Rmpy > 5) Rmpy = 5;
      if (Gmpy > 5) Gmpy = 5;
      if (Bmpy > 5) Bmpy = 5;
      zdialog_stuff(zd,"Rmpy",Rmpy);
      zdialog_stuff(zd,"Gmpy",Gmpy);
      zdialog_stuff(zd,"Bmpy",Bmpy);
   }
   
   zdialog_fetch(zd,"Rdark",Rdark);                                              //  get all params
   zdialog_fetch(zd,"Gdark",Gdark);
   zdialog_fetch(zd,"Bdark",Bdark);
   zdialog_fetch(zd,"Rbrite",Rbrite);
   zdialog_fetch(zd,"Gbrite",Gbrite);
   zdialog_fetch(zd,"Bbrite",Bbrite);
   zdialog_fetch(zd,"Rmpy",Rmpy);
   zdialog_fetch(zd,"Gmpy",Gmpy);
   zdialog_fetch(zd,"Bmpy",Bmpy);
   
   Fchange = 0;
   if (Rdark != pRdark) Fchange = 1;                                             //  detect changed params
   if (Gdark != pGdark) Fchange = 1;
   if (Bdark != pBdark) Fchange = 1;
   if (Rbrite != pRbrite) Fchange = 1;
   if (Gbrite != pGbrite) Fchange = 1;
   if (Bbrite != pBbrite) Fchange = 1;
   if (Rmpy != pRmpy) Fchange = 1;
   if (Gmpy != pGmpy) Fchange = 1;
   if (Bmpy != pBmpy) Fchange = 1;
   
   pRdark = Rdark;                                                               //  remember values for change detection
   pGdark = Gdark;
   pBdark = Bdark;
   pRbrite = Rbrite; 
   pGbrite = Gbrite; 
   pBbrite = Bbrite; 
   pRmpy = Rmpy; 
   pGmpy = Gmpy; 
   pBmpy = Bmpy; 

//  wait_thread_idle() removed   20.0

   if (Fchange) {                                                                //  global params changed
      thread_command = "global";
      signal_thread();                                                           //  update image
      Fchange = 0;
   }
   
   if (zstrstr("blend, reduce bright",event)) {                                  //  blend params changed
      zdialog_fetch(zd,"blend",blend);
      zdialog_fetch(zd,"reduce bright",reducebright);
      thread_command = "blend";
      signal_thread();                                                           //  update image
   }

   return 1;
}


//  get dark point or bright point from mouse click position

void gretinex_mousefunc()
{
   using namespace gretinex_names;
   
   int         px, py, dx, dy;
   float       red, green, blue;
   float       *ppix;
   char        mousetext[60];
   zdialog     *zd = EFgretinex.zd;
   
   if (! zd) {
      freeMouse();
      return;
   }

   if (! Fbrightpoint && ! Fdarkpoint) {
      freeMouse();
      return;
   }

   if (! LMclick) return;
   LMclick = 0;

   px = Mxclick;                                                                 //  mouse click position
   py = Myclick;

   if (px < 2) px = 2;                                                           //  pull back from edge
   if (px > E3pxm->ww-3) px = E3pxm->ww-3;
   if (py < 2) py = 2;
   if (py > E3pxm->hh-3) py = E3pxm->hh-3;

   red = green = blue = 0;

   for (dy = -1; dy <= 1; dy++)                                                  //  3x3 block around mouse position
   for (dx = -1; dx <= 1; dx++)
   {
      ppix = PXMpix(E1pxm,px+dx,py+dy);                                          //  input image
      red += ppix[0];
      green += ppix[1];
      blue += ppix[2];
   }

   red = red / 9.0;                                                              //  mean RGB levels
   green = green / 9.0;
   blue = blue / 9.0;

   snprintf(mousetext,60,"3x3 pixels RGB: %.0f %.0f %.0f \n",red,green,blue);
   poptext_mouse(mousetext,10,10,0,3);
   
   if (Fbrightpoint) {                                                           //  click pixel is new bright point
      Rbrite= red;
      Gbrite = green;
      Bbrite = blue;
      zdialog_stuff(zd,"Rbrite",Rbrite);                                         //  stuff values into dialog
      zdialog_stuff(zd,"Gbrite",Gbrite);
      zdialog_stuff(zd,"Bbrite",Bbrite);
   }

   if (Fdarkpoint) {                                                             //  click pixel is new dark point
      Rdark = red;
      Gdark = green;
      Bdark = blue;
      zdialog_stuff(zd,"Rdark",Rdark);                                           //  stuff values into dialog
      zdialog_stuff(zd,"Gdark",Gdark);
      zdialog_stuff(zd,"Bdark",Bdark);
   }
   
   if (Fbrightpoint || Fdarkpoint) {
      thread_command = "global";
      signal_thread();                                                           //  trigger image update
      wait_thread_idle();
   }
   
   return;
}


//  thread function - multiple working threads to update image

void * gretinex_thread(void *)
{
   using namespace gretinex_names;

   void  * gretinex_wthread1(void *arg);                                         //  worker threads
   void  * gretinex_wthread2(void *arg);
   void  * gretinex_wthread3(void *arg);
   
   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      if (strmatch(thread_command,"autoscale")) {
         gretinex_wthread1(0);                                                   //  worker thread for autoscale
         thread_command = "global";
      }
      
      if (strmatch(thread_command,"global"))
      {
         do_wthreads(gretinex_wthread2,NWT);                                     //  worker thread for image RGB rescale

         if (E9pxm) PXM_free(E9pxm);                                             //  E9 image = global retinex output
         E9pxm = PXM_copy(E3pxm);

         thread_command = "blend";                                               //  auto blend after global retinex
      }
      
      if (strmatch(thread_command,"blend"))
      {
         if (! E9pxm) E9pxm = PXM_copy(E3pxm);                                   //  stop thread crash                  19.0
         do_wthreads(gretinex_wthread3,NWT);                                     //  worker thread for image blend
      }

      CEF->Fmods++;                                                              //  image modified
      CEF->Fsaved = 0;

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

   return 0;                                                                     //  not executed, stop warning
}


//  worker thread function - autoscale retinex

void * gretinex_wthread1(void *)
{
   using namespace gretinex_names;
   
   int      ii, dist = 0;
   int      px, py, dx, dy;
   int      red, green, blue;
   float    *pix1;

   Rdark = Gdark = Bdark = 255;
   Rbrite = Gbrite = Bbrite = 0;
   Rmpy = Gmpy = Bmpy = 1.0;
   Fbrightpoint = Fdarkpoint = 0;

   for (py = 1; py < E1pxm->hh-1; py++)
   for (px = 1; px < E1pxm->ww-1; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E1pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside area
      }

      red = green = blue = 0;

      for (dy = -1; dy <= 1; dy++)                                               //  3x3 block around mouse position
      for (dx = -1; dx <= 1; dx++)
      {
         pix1 = PXMpix(E1pxm,px+dx,py+dy);                                       //  input image
         red += pix1[0];
         green += pix1[1];
         blue += pix1[2];
      }

      red = red / 9.0;                                                           //  mean RGB levels
      green = green / 9.0;
      blue = blue / 9.0;
      
      if (red < Rdark) Rdark = red;                                              //  update limits
      if (green < Gdark) Gdark = green;
      if (blue < Bdark) Bdark = blue;
      if (red > Rbrite) Rbrite = red;
      if (green > Gbrite) Gbrite = green;
      if (blue > Bbrite) Bbrite = blue;
   }

   return 0;                                                                     //  not executed, avoid gcc warning
}


//  worker thread function - scale image RGB values

void * gretinex_wthread2(void *arg)
{
   using namespace gretinex_names;
   
   int      ii, index, dist = 0;
   int      px, py;
   float    R1, G1, B1, R3, G3, B3;
   float    f1, f2, F, cmax;
   float    *pix1, *pix3;

   index = *((int *) arg);
   
   for (py = index; py < E3pxm->hh; py += NWT)                                   //  loop all image pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside area
      }

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

      R1 = pix1[0];                                                              //  input RGB values
      G1 = pix1[1];
      B1 = pix1[2];
      
      R1 = 255 * (R1 - Rdark) / (Rbrite - Rdark);                                //  rescale for full 0-255 range
      G1 = 255 * (G1 - Gdark) / (Gbrite - Gdark);
      B1 = 255 * (B1 - Bdark) / (Bbrite - Bdark);

      if (R1 < 0) R1 = 0;
      if (G1 < 0) G1 = 0;
      if (B1 < 0) B1 = 0;

      R3 = R1 * Rmpy;
      G3 = G1 * Gmpy;
      B3 = B1 * Bmpy;

      if (R3 > 255 || G3 > 255 || B3 > 255) {                                    //  stop overflow
         cmax = R3;
         if (G3 > cmax) cmax = G3;
         if (B3 > cmax) cmax = B3;
         F = 255 / cmax;
         R3 *= F;
         G3 *= F;
         B3 *= F;
      }

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         R3 = f1 * R3 + f2 * R1;
         G3 = f1 * G3 + f2 * G1;
         B3 = f1 * B3 + f2 * B1;
      }

      pix3[0] = R3;                                                              //  output RGB values
      pix3[1] = G3;
      pix3[2] = B3;
   }

   pthread_exit(0);
}


//  worker thread function - blend input and output images, attenuate bright pixels

void * gretinex_wthread3(void *arg)
{
   using namespace gretinex_names;

   int      index = *((int *) arg);
   int      px, py;
   int      ii, dist = 0;
   float    *pix1, *pix2, *pix3;
   float    R1, G1, B1, R2, G2, B2, R3, G3, B3;
   float    F1, F2, Lmax;
   
   for (py = index; py < e3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < e3ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * e3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside area
      }
      
      pix1 = PXMpix(E1pxm,px,py);                                                //  input image pixel
      pix2 = PXMpix(E9pxm,px,py);                                                //  global retinex image pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output image pixel

      R1 = pix1[0];                                                              //  input color - original image
      G1 = pix1[1];
      B1 = pix1[2];

      R2 = pix2[0];                                                              //  input color - retinex image
      G2 = pix2[1];
      B2 = pix2[2];

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         F1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         F2 = 1.0 - F1;
         R2 = F1 * R2 + F2 * R1;
         G2 = F1 * G2 + F2 * G1;
         B2 = F1 * B2 + F2 * B1;
      }

      Lmax = R1;                                                                 //  max. RGB input
      if (G1 > Lmax) Lmax = G1;
      if (B1 > Lmax) Lmax = B1;
      
      F1 = blend;                                                                //  0 ... 1  >>  E1 ... E3
      F2 = reducebright;                                                         //  0 ... 1
      F1 = F1 * (1.0 - F2 * Lmax / 255.0);                                       //  reduce F1 for high F2 * Lmax

      R3 = F1 * R2 + (1.0 - F1) * R1;                                            //  output RGB = blended inputs
      G3 = F1 * G2 + (1.0 - F1) * G1;
      B3 = F1 * B2 + (1.0 - F1) * B1;
      
      pix3[0] = R3;                                                              //  output RGB values
      pix3[1] = G3;
      pix3[2] = B3;
   }

   pthread_exit(0);
}


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

//  Zonal Retinex function
//  Rescale RGB values based on surrounding zones: increase local brightness range.

namespace zretinex_names 
{
   editfunc    EFzretinex;                                                       //  edit function data
   int         e3ww, e3hh;
   cchar       *thread_command;
   float       blend, reducedark, reducebright;
   
   int         maxzones = 10000;
   int         Nzones = 100, Pzones = 0;                                         //  zone count, 1-10000
   int         zsize, zrows, zcols, zww, zhh;                                    //  zone data

   typedef struct {                                                              //  zone structure
      int      cx, cy;                                                           //  zone center in image
      float    minR, minG, minB;                                                 //  RGB minimum values in zone
      float    maxR, maxG, maxB;                                                 //  RGB mazimum values in zone
   }           zone_t;

   zone_t      *zones = 0;                                                       //  up to 10000 zones

   int16       *zoneindex = 0;                                                   //  zoneindex[ii][z] pixel ii, 25 zones
   int16       *zoneweight = 0;                                                  //  zoneweight[ii][z] zone weights
}                                                                                //  float -> int16 to save memory      20.0


//  menu function

void m_zretinex(GtkWidget *, cchar *menu)
{
   using namespace zretinex_names;

   int    zretinex_dialog_event(zdialog *zd, cchar *event);
   void * zretinex_thread(void *);

   F1_help_topic = "zonal retx";

   EFzretinex.menuname = menu;
   EFzretinex.menufunc = m_zretinex;
   EFzretinex.funcname = "Zretinex";                                             //  function name
   EFzretinex.Farea = 2;                                                         //  select area usable
   EFzretinex.Frestart = 1;                                                      //  allow restart
   EFzretinex.Fscript = 1;                                                       //  scripting supported
   EFzretinex.threadfunc = zretinex_thread;                                      //  thread function

   if (! edit_setup(EFzretinex)) return;                                         //  setup edit

   e3ww = E3pxm->ww;                                                             //  image size
   e3hh = E3pxm->hh;

   E9pxm = PXM_copy(E1pxm);
   
/***
          ______________________________________
         |            Zonal Retinex             |
         |                                      |
         | zone count: [___]  [apply]           |
         | blend: =======[]===================  |
         | reduce dark: ==========[]==========  |
         | reduce bright: ==========[]========  |
         |                                      |
         |             [reset] [done] [cancel]  |
         |______________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Zonal Retinex"),Mwin,Breset,Bdone,Bcancel,null); 
   EFzretinex.zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbz","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labzs","hbz",E2X("zone count:"),"space=5");
   zdialog_add_widget(zd,"zspin","zone count","hbz","20|10000|10|100");
   zdialog_add_widget(zd,"button","apply","hbz",Bapply,"space=3");
   zdialog_add_widget(zd,"hbox","hbb","dialog");
   zdialog_add_widget(zd,"label","labb","hbb",E2X("blend"),"space=5");
   zdialog_add_widget(zd,"hscale","blend","hbb","0|1.0|0.01|1.0","expand");
   zdialog_add_widget(zd,"hbox","hbrd","dialog");
   zdialog_add_widget(zd,"label","labrd","hbrd",E2X("reduce dark"),"space=5");
   zdialog_add_widget(zd,"hscale","reduce dark","hbrd","0|1.0|0.01|0.0","expand");
   zdialog_add_widget(zd,"hbox","hbrl","dialog");
   zdialog_add_widget(zd,"label","labrd","hbrl",E2X("reduce bright"),"space=5");
   zdialog_add_widget(zd,"hscale","reduce bright","hbrl","0|1.0|0.01|0.0","expand");

   zdialog_run(zd,zretinex_dialog_event,"save");                                 //  run dialog - parallel
   zdialog_send_event(zd,"reset");
   return;
}


//  dialog event and completion function

int zretinex_dialog_event(zdialog *zd, cchar *event)
{
   using namespace zretinex_names;

   void   zretinex_zonesetup(zdialog *zd);
   
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"reset")) zd->zstat = 1;                                   //  initz. 
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         blend = 1.0;
         reducedark = 0.0;
         reducebright = 0.0;
         edit_reset();
         PXM_free(E9pxm);
         E9pxm = PXM_copy(E1pxm);
         return 1;
      }

      else if (zd->zstat == 2) {                                                 //  done
         edit_done(0);                                                           //  commit edit
         PXM_free(E9pxm);
         if (zones) zfree(zones);
         if (zoneindex) zfree(zoneindex);
         if (zoneweight) zfree(zoneweight);
         zones = 0;
         zoneindex = 0;
         zoneweight = 0;
         return 1;
      }

      else {
         edit_cancel(0);                                                         //  discard edit
         PXM_free(E9pxm);
         if (zones) zfree(zones);
         if (zoneindex) zfree(zoneindex);
         if (zoneweight) zfree(zoneweight);
         zones = 0;
         zoneindex = 0;
         zoneweight = 0;
         return 1;
      }
   }

   if (strmatch(event,"apply")) {                                                //  get new zone count
      zretinex_zonesetup(zd);                                                    //  zone setups
      thread_command = "apply";
      signal_thread();                                                           //  update image
      wait_thread_idle();
   }

   if (strmatch(event,"blend")) {                                                //  blend param changed
      zdialog_fetch(zd,"blend",blend);
      thread_command = "blend";
      signal_thread();                                                           //  update image
      wait_thread_idle();
   }

   if (zstrstr(event,"reduce")) {                                                //  reduce param changed
      zdialog_fetch(zd,"reduce dark",reducedark);
      zdialog_fetch(zd,"reduce bright",reducebright);
      thread_command = "blend";
      signal_thread();                                                           //  update image
      wait_thread_idle();
   }

   return 1;
}


//  setup new zone parameters and allocate memory

void zretinex_zonesetup(zdialog *zd)
{
   using namespace zretinex_names;

   int      goal, dirc;
   int      row, col, xlo, ylo;
   int      ii, nn, cx, cy;

   Pzones = Nzones;                                                              //  prior count
   zdialog_fetch(zd,"zone count",Nzones);                                        //  new count

   goal = Nzones;                                                                //  new zone count goal
   dirc = Nzones - Pzones;                                                       //  direction of change
   if (dirc == 0) dirc = 1;

   while (true)
   {
      zsize = sqrt(e3ww * e3hh / goal);                                          //  approx. zone size
      zrows = e3hh / zsize;                                                      //  get appropriate rows and cols
      zcols = e3ww / zsize;
      if (zrows < 1) zrows = 1;
      if (zcols < 1) zcols = 1;
      Nzones = zrows * zcols;                                                    //  matching rows x cols

      if (dirc > 0 && Nzones <= Pzones) {                                        //  no increase, try again
         if (Nzones == maxzones) break;
         goal++;
         continue;
      }

      if (dirc < 0 && Nzones >= Pzones) {                                        //  no decrease, try again
         if (Nzones == 1) break;
         goal--;
         continue;
      }

      if (dirc > 0 && Nzones > Pzones) break;                                    //  done
      if (dirc < 0 && Nzones < Pzones) break;
   }
   
   zdialog_stuff(zd,"zone count",Nzones);                                        //  update zdialog widget

   if (zones) zfree(zones);                                                      //  allocate zone memory
   zones = (zone_t *) zmalloc(Nzones * sizeof(zone_t));

   zww = e3ww / zcols;                                                           //  zone width, height
   zhh = e3hh / zrows;

   nn = e3ww * e3hh * 25;                                                        //  25 neighbor zones per pixel

   if (zoneindex) zfree(zoneindex);                                              //  allocate pixel zone index
   zoneindex = (int16 *) zmalloc(nn * sizeof(int));                              //  20.0

   if (zoneweight) zfree(zoneweight);                                            //  allocate pixel zone weight
   zoneweight = (int16 *) zmalloc(nn * sizeof(float));                           //  20.0

   for (row = 0; row < zrows; row++)                                             //  loop all zones
   for (col = 0; col < zcols; col++)
   {
      xlo = col * zww;                                                           //  zone low pixel
      ylo = row * zhh;
      cx = xlo + zww/2;                                                          //  zone center pixel
      cy = ylo + zhh/2;
      ii = row * zcols + col;                                                    //  zone index
      zones[ii].cx = cx;                                                         //  zone center
      zones[ii].cy = cy;
   }

   return;
}


//  thread function - multiple working threads to update image

void * zretinex_thread(void *)
{
   using namespace zretinex_names;

   void * zretinex_wthread1(void *arg);
   void * zretinex_wthread2(void *arg);
   void * zretinex_wthread3(void *arg);
   
   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request

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

      if (strmatch(thread_command,"apply"))                                      //  update zones and image
      {
         do_wthreads(zretinex_wthread1,NWT);                                     //  compute zone RGB levels and weights
         do_wthreads(zretinex_wthread2,NWT);                                     //  compute new pixel RGB values
         thread_command = "blend";                                               //  auto blend after
      }
      
      if (strmatch(thread_command,"blend"))
         do_wthreads(zretinex_wthread3,NWT);                                     //  blend input and retinex images

      CEF->Fmods++;                                                              //  image modified
      CEF->Fsaved = 0;

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

   return 0;                                                                     //  not executed, stop warning
}


//  worker thread to compute zone RGB levels and weights per image pixel

void * zretinex_wthread1(void *arg)
{
   using namespace zretinex_names;

   int      index = *((int *) arg);
   int      px, py, row, col;
   int      xlo, xhi, ylo, yhi, cx, cy;
   int      ii, jj, kk;
   float    minR, minG, minB, maxR, maxG, maxB;
   float    dist, maxdist, weight, sumweight;
   float    *pix;

   for (row = index; row < zrows; row += NWT)                                    //  loop all zones
   for (col = 0; col < zcols; col++)
   {
      xlo = col * zww;                                                           //  zone low pixel
      ylo = row * zhh;
      xhi = xlo + zww;                                                           //  zone high pixel
      yhi = ylo + zhh;

      minR = minG = minB = 256;
      maxR = maxG = maxB = 0;

      for (py = ylo; py < yhi; py++)                                             //  find min/max RGB in zone
      for (px = xlo; px < xhi; px++)
      {
         pix = PXMpix(E1pxm,px,py);
         if (pix[0] < minR) minR = pix[0];
         if (pix[1] < minG) minG = pix[1];
         if (pix[2] < minB) minB = pix[2];
         if (pix[0] > maxR) maxR = pix[0];
         if (pix[1] > maxG) maxG = pix[1];
         if (pix[2] > maxB) maxB = pix[2];
      }
      
      ii = row * zcols + col;                                                    //  zone index
      zones[ii].minR = minR;                                                     //  zone RGB range
      zones[ii].minG = minG;
      zones[ii].minB = minB;
      zones[ii].maxR = maxR;
      zones[ii].maxG = maxG;
      zones[ii].maxB = maxB;
   }

   for (py = index; py < e3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < e3ww; px++)
   {
      row = py / zhh;                                                            //  zone containing pixel
      col = px / zww;

      ii = (py * e3ww + px) * 25;                                                //  base zone index for pixel
      
      for (jj = row-2; jj <= row+2; jj++)                                        //  loop 25 neighboring zones
      for (kk = col-2; kk <= col+2; kk++)
      {
         if (jj >= 0 && jj < zrows && kk >= 0 && kk < zcols)                     //  neighbor zone number
            zoneindex[ii] = jj * zcols + kk;                                     //    --> pixel zone index            
         else zoneindex[ii] = -1;                                                //  pixel zone on edge, missing neighbor
         ii++;
      }
   }
   
   if (zww < zhh) maxdist = 2.5 * zww;
   else maxdist = 2.5 * zhh;

   for (py = index; py < e3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < e3ww; px++)
   {
      row = py / zhh;                                                            //  zone containing pixel
      col = px / zww;

      ii = (py * e3ww + px) * 25;                                                //  base zone index for pixel
      
      for (jj = 0; jj < 25; jj++)                                                //  loop neighbor zones
      {
         kk = zoneindex[ii+jj];
         if (kk < 0) {                                                           //  neighbor missing 
            zoneweight[ii+jj] = 0;
            continue;
         }
         cx = zones[kk].cx;                                                      //  zone center
         cy = zones[kk].cy;
         dist = sqrtf((px-cx)*(px-cx) + (py-cy)*(py-cy));                        //  distance from (px,py)
         weight = 1.0 - dist / maxdist;                                          //  dist 0..max --> weight 1..0
         if (weight < 0) weight = 0;
         zoneweight[ii+jj] = 1000.0 * weight;                                    //  scale 1.0 = 1000                   20.0
      }

      sumweight = 0;
      for (jj = 0; jj < 25; jj++)                                                //  get sum of zone weights
         sumweight += zoneweight[ii+jj];
      
      for (jj = 0; jj < 25; jj++)                                                //  make weights add up to 1.0
         zoneweight[ii+jj] = 1000.0 * zoneweight[ii+jj] / sumweight;             //  1000 = 1.0                         20.0
   }

   pthread_exit(0);
}


//  worker thread to compute new image RGB values

void * zretinex_wthread2(void *arg)
{
   using namespace zretinex_names;

   int      index = *((int *) arg);
   int      px, py, ii, jj, kk, dist = 0;
   float    minR, minG, minB, maxR, maxG, maxB;
   float    R1, G1, B1, R2, G2, B2;
   float    weight, F, f1, f2;
   float    *pix1, *pix2;
   
   for (py = index; py < e3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < e3ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * e3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside area
      }

      minR = minG = minB = 0;
      maxR = maxG = maxB = 0;
      
      ii = (py * e3ww + px) * 25;                                                //  base zone index for pixel
      
      for (jj = 0; jj < 25; jj++)                                                //  loop neighbor zones
      {
         kk = zoneindex[ii+jj];                                                  //  zone number
         if (kk < 0) continue;
         weight = 0.001 * zoneweight[ii+jj];                                     //  zone weight (1000 = 1.0)           20.0
         minR += weight * zones[kk].minR;                                        //  sum weighted RGB range
         minG += weight * zones[kk].minG;                                        //    for neighbor zones
         minB += weight * zones[kk].minB;
         maxR += weight * zones[kk].maxR;
         maxG += weight * zones[kk].maxG;
         maxB += weight * zones[kk].maxB;
      }
      
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix2 = PXMpix(E9pxm,px,py);                                                //  output pixel

      R1 = pix1[0];
      G1 = pix1[1];
      B1 = pix1[2];
      
      R2 = 255 * (R1 - minR) / (maxR - minR + 1);                                //  avoid poss. divide by zero
      G2 = 255 * (G1 - minG) / (maxG - minG + 1);
      B2 = 255 * (B1 - minB) / (maxB - minB + 1);

      if (R2 < 0) R2 = 0;                                                        //  stop underflow
      if (G2 < 0) G2 = 0;
      if (B2 < 0) B2 = 0;
      
      if (R2 > 255 || G2 > 255 || B2 > 255) {                                    //  stop overflow
         F = R2;
         if (G2 > F) F = G2;
         if (B2 > F) F = B2;
         F = 255 / F;
         R2 *= F;
         G2 *= F;
         B2 *= F;
      }

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         R2 = f1 * R2 + f2 * R1;
         G2 = f1 * G2 + f2 * G1;
         B2 = f1 * B2 + f2 * B1;
      }

      pix2[0] = R2;
      pix2[1] = G2;
      pix2[2] = B2;
   }

   pthread_exit(0);
}


//  worker thread to blend input image with retinex image
//  and attenuate bright pixels

void * zretinex_wthread3(void *arg)
{
   using namespace zretinex_names;

   int      index = *((int *) arg);
   int      px, py;
   int      ii, dist = 0;
   float    *pix1, *pix2, *pix3;
   float    R1, G1, B1, R2, G2, B2, R3, G3, B3;
   float    F1, F2, Lmax;
   
   for (py = index; py < e3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < e3ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * e3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside area
      }

      pix1 = PXMpix(E1pxm,px,py);                                                //  input image pixel
      pix2 = PXMpix(E9pxm,px,py);                                                //  retinex image pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output image pixel

      R1 = pix1[0];                                                              //  input color - original image
      G1 = pix1[1];
      B1 = pix1[2];

      R2 = pix2[0];                                                              //  input color - retinex image
      G2 = pix2[1];
      B2 = pix2[2];

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         F1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         F2 = 1.0 - F1;
         R2 = F1 * R2 + F2 * R1;
         G2 = F1 * G2 + F2 * G1;
         B2 = F1 * B2 + F2 * B1;
      }

      Lmax = R1;                                                                 //  max. RGB input
      if (G1 > Lmax) Lmax = G1;
      if (B1 > Lmax) Lmax = B1;
      Lmax = Lmax / 255.0;                                                       //  scale 0 - 1
      
      F1 = blend;                                                                //  0 ... 1  >>  E1 ... E3

      F2 = reducedark;
      F1 = F1 * (1.0 - F2 * (1.0 - Lmax));                                       //  reduce F1 for high F2 * (1 - Lmax)

      F2 = reducebright;                                                         //  0 ... 1
      F1 = F1 * (1.0 - F2 * Lmax);                                               //  reduce F1 for high F2 * Lmax

      R3 = F1 * R2 + (1.0 - F1) * R1;                                            //  output RGB = blended inputs
      G3 = F1 * G2 + (1.0 - F1) * G1;
      B3 = F1 * B2 + (1.0 - F1) * B1;
      
      pix3[0] = R3;                                                              //  output RGB values
      pix3[1] = G3;
      pix3[2] = B3;
   }

   pthread_exit(0);
}


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

//  image sharpen functions

namespace sharpen_names
{
   int      E3ww, E3hh;                                                          //  image dimensions
   int      UM_radius, UM_amount, UM_thresh;
   int      GR_amount, GR_thresh;
   int      KH_radius;
   int      MD_radius, MD_dark, MD_light, *MD_britemap;
   char     sharp_function[4] = "";

   int      brhood_radius;
   float    brhood_kernel[200][200];                                             //  up to radius = 99
   char     brhood_method;                                                       //  g = gaussian, f = flat distribution
   float    *brhood_brightness;                                                  //  neighborhood brightness per pixel

   VOL int  sharp_cancel, brhood_cancel;                                         //  avoid GCC optimizing code away

   editfunc    EFsharp;
}


//  menu function

void m_sharpen(GtkWidget *, cchar *menu)
{
   using namespace sharpen_names;

   int    sharp_dialog_event(zdialog *zd, cchar *event);
   void * sharp_thread(void *);
   int    ii;

   F1_help_topic = "sharpen";

   EFsharp.menuname = menu;
   EFsharp.menufunc = m_sharpen;
   EFsharp.funcname = "sharpen";
   EFsharp.Farea = 2;                                                            //  select area usable
   EFsharp.threadfunc = sharp_thread;                                            //  thread function
   EFsharp.Frestart = 1;                                                         //  allow restart
   EFsharp.FusePL = 1;                                                           //  use with paint/lever edits OK
   EFsharp.Fscript = 1;                                                          //  scripting supported
   if (! edit_setup(EFsharp)) return;                                            //  setup edit

   E3ww = E3pxm->ww;                                                             //  image dimensions
   E3hh = E3pxm->hh;

/***
          _________________________________________
         |                 Sharpen                 |
         |                                         |
         |  [_] unsharp mask        radius  [__]   |
         |                          amount  [__]   |
         |                        threshold [__]   |
         |                                         |
         |  [_] gradient            amount  [__]   |
         |                        threshold [__]   |
         |                                         |
         |  [_] Kuwahara            radius  [__]   |
         |                                         |
         |  [_] median diff         radius  [__]   |
         |                           dark   [__]   |
         |                           light  [__]   |
         |                                         |
         |        [reset] [apply] [done] [cancel]  |
         |_________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Sharpen"),Mwin,Breset,Bapply,Bdone,Bcancel,null);
   EFsharp.zd = zd;

   zdialog_add_widget(zd,"hbox","hbum","dialog",0,"space=5");                    //  unsharp mask
   zdialog_add_widget(zd,"vbox","vb21","hbum",0,"space=2");
   zdialog_add_widget(zd,"label","space","hbum",0,"expand");
   zdialog_add_widget(zd,"vbox","vb22","hbum",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vb23","hbum",0,"homog|space=2");
   zdialog_add_widget(zd,"check","UM","vb21",E2X("unsharp mask"),"space=5");
   zdialog_add_widget(zd,"label","lab21","vb22",Bradius);
   zdialog_add_widget(zd,"label","lab22","vb22",Bamount);
   zdialog_add_widget(zd,"label","lab23","vb22",Bthresh);
   zdialog_add_widget(zd,"zspin","radiusUM","vb23","1|20|1|2");
   zdialog_add_widget(zd,"zspin","amountUM","vb23","1|200|1|100");
   zdialog_add_widget(zd,"zspin","threshUM","vb23","1|100|1|0");

   zdialog_add_widget(zd,"hsep","sep3","dialog");                                //  gradient
   zdialog_add_widget(zd,"hbox","hbgr","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb31","hbgr",0,"space=2");
   zdialog_add_widget(zd,"label","space","hbgr",0,"expand");
   zdialog_add_widget(zd,"vbox","vb32","hbgr",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vb33","hbgr",0,"homog|space=2");
   zdialog_add_widget(zd,"check","GR","vb31","gradient","space=5");
   zdialog_add_widget(zd,"label","lab32","vb32",Bamount);
   zdialog_add_widget(zd,"label","lab33","vb32",Bthresh);
   zdialog_add_widget(zd,"zspin","amountGR","vb33","1|400|1|100");
   zdialog_add_widget(zd,"zspin","threshGR","vb33","1|100|1|0");

   zdialog_add_widget(zd,"hsep","sep4","dialog");                                //  kuwahara
   zdialog_add_widget(zd,"hbox","hbku","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","KH","hbku","Kuwahara","space=3");
   zdialog_add_widget(zd,"label","space","hbku",0,"expand");
   zdialog_add_widget(zd,"label","lab42","hbku",Bradius,"space=3");
   zdialog_add_widget(zd,"zspin","radiusKH","hbku","1|9|1|1");

   zdialog_add_widget(zd,"hsep","sep5","dialog");                                //  median diff
   zdialog_add_widget(zd,"hbox","hbmd","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb51","hbmd",0,"space=2");
   zdialog_add_widget(zd,"label","space","hbmd",0,"expand");
   zdialog_add_widget(zd,"vbox","vb52","hbmd",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vb53","hbmd",0,"homog|space=2");
   zdialog_add_widget(zd,"check","MD","vb51",E2X("median diff"),"space=5");
   zdialog_add_widget(zd,"label","lab51","vb52",Bradius);
   zdialog_add_widget(zd,"label","lab52","vb52",E2X("dark"));
   zdialog_add_widget(zd,"label","lab53","vb52",E2X("light"));
   zdialog_add_widget(zd,"zspin","radiusMD","vb53","1|20|1|3");
   zdialog_add_widget(zd,"zspin","darkMD","vb53","0|50|1|1");
   zdialog_add_widget(zd,"zspin","lightMD","vb53","0|50|1|1");

   zdialog_restore_inputs(zd);
   
   zdialog_fetch(zd,"UM",ii);                                                    //  set function from checkboxes
   if (ii) strcpy(sharp_function,"UM");
   zdialog_fetch(zd,"GR",ii);
   if (ii) strcpy(sharp_function,"GR");
   zdialog_fetch(zd,"KH",ii);
   if (ii) strcpy(sharp_function,"KH");
   zdialog_fetch(zd,"MD",ii);
   if (ii) strcpy(sharp_function,"MD");

   zdialog_run(zd,sharp_dialog_event,"save");                                    //  run dialog - parallel

   return;
}


//  dialog event and completion callback function

int sharp_dialog_event(zdialog *zd, cchar *event)                                //  reworked for script files
{
   using namespace sharpen_names;
   
   if (strmatch(event,"focus")) return 1;

   zdialog_fetch(zd,"radiusUM",UM_radius);                                       //  get all parameters
   zdialog_fetch(zd,"amountUM",UM_amount);
   zdialog_fetch(zd,"threshUM",UM_thresh);
   zdialog_fetch(zd,"amountGR",GR_amount);
   zdialog_fetch(zd,"threshGR",GR_thresh);
   zdialog_fetch(zd,"radiusKH",KH_radius);
   zdialog_fetch(zd,"radiusMD",MD_radius);
   zdialog_fetch(zd,"darkMD",MD_dark);
   zdialog_fetch(zd,"lightMD",MD_light);

   if (strmatch(event,"apply")) zd->zstat = 2;                                   //  from script file 
   if (strmatch(event,"done")) zd->zstat = 3;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  from f_open()
   
   sharp_cancel = 0;

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;
         edit_reset();
         return 1;
      }

      if (zd->zstat == 2) {                                                      //  apply
         zd->zstat = 0;
         if (*sharp_function) signal_thread();
         else zmessageACK(Mwin,Bnoselection);                                    //  no choice made
         return 1;
      }

      if (zd->zstat == 3) {
         edit_done(0);                                                           //  done
         return 1;
      }

      sharp_cancel = 1;                                                          //  cancel or [x]
      edit_cancel(0);                                                            //  discard edit
      return 1;
   }

   if (strmatch(event,"blendwidth")) signal_thread();

   if (strmatchV(event,"UM","GR","KH","MD",null))
   {
      zdialog_stuff(zd,"UM",0);                                                  //  make checkboxes like radio buttons
      zdialog_stuff(zd,"GR",0);
      zdialog_stuff(zd,"KH",0);
      zdialog_stuff(zd,"MD",0);
      zdialog_stuff(zd,event,1);
      strcpy(sharp_function,event);                                              //  set chosen method
   }

   return 1;
}


//  sharpen image thread function

void * sharp_thread(void *)
{
   using namespace sharpen_names;

   int sharp_UM(void);
   int sharp_GR(void);
   int sharp_KH(void);
   int sharp_MD(void);

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

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

      if (strmatch(sharp_function,"UM")) sharp_UM();
      if (strmatch(sharp_function,"GR")) sharp_GR();
      if (strmatch(sharp_function,"KH")) sharp_KH();
      if (strmatch(sharp_function,"MD")) sharp_MD();

      CEF->Fmods++;
      CEF->Fsaved = 0;

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

   return 0;                                                                     //  not executed, stop g++ warning
}


//  image sharpen function using unsharp mask

int sharp_UM()
{
   using namespace sharpen_names;

   void  britehood(int radius, char method);                                     //  compute neighborhood brightness
   void * sharp_UM_wthread(void *arg);

   int      cc;

   cc = E3ww * E3hh * sizeof(float);
   brhood_brightness = (float *) zmalloc(cc);

   britehood(UM_radius,'f');
   if (sharp_cancel) return 1;

   if (sa_stat == 3) Fbusy_goal = sa_Npixel;
   else  Fbusy_goal = E3ww * E3hh;
   Fbusy_done = 0;

   do_wthreads(sharp_UM_wthread,NWT);                                            //  worker threads

   Fbusy_goal = 0;

   zfree(brhood_brightness);
   return 1;
}


void * sharp_UM_wthread(void *arg)                                               //  worker thread function
{
   using namespace sharpen_names;

   int         index = *((int *) arg);
   int         px, py, ii, dist = 0;
   float       amount, thresh, bright;
   float       mean, incr, ratio, f1, f2;
   float       red1, green1, blue1, red3, green3, blue3;
   float       *pix1, *pix3;

   for (py = index; py < E3hh; py += NWT)                                        //  loop all image3 pixels
   for (px = 0; px < E3ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      amount = 0.01 * UM_amount;                                                 //  0.0 to 2.0
      thresh = 0.4 * UM_thresh;                                                  //  0 to 40 (256 max. possible)

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

      bright = pixbright(pix1);
      if (bright < 1) continue;                                                  //  effectively black
      ii = py * E3ww + px;
      mean = brhood_brightness[ii];

      incr = (bright - mean);
      if (fabsf(incr) < thresh) continue;                                        //  omit low-contrast pixels

      incr = incr * amount;                                                      //  0.0 to 2.0
      if (bright + incr > 255) incr = 255 - bright;
      ratio = (bright + incr) / bright;
      if (ratio < 0) ratio = 0;

      red1 = pix1[0];                                                            //  input RGB
      green1 = pix1[1];
      blue1 = pix1[2];

      red3 = ratio * red1;                                                       //  output RGB
      if (red3 > 255) red3 = 255;
      green3 = ratio * green1;
      if (green3 > 255) green3 = 255;
      blue3 = ratio * blue1;
      if (blue3 > 255) blue3 = 255;

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         red3 = f1 * red3 + f2 * red1;
         green3 = f1 * green3 + f2 * green1;
         blue3 = f1 * blue3 + f2 * blue1;
      }

      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;

      Fbusy_done++;                                                              //  track progress
      if (sharp_cancel) break;
   }

   pthread_exit(0);
}


//  sharpen image by increasing brightness gradient

int sharp_GR()
{
   using namespace sharpen_names;

   void * sharp_GR_wthread(void *arg);

   if (sa_stat == 3) Fbusy_goal = sa_Npixel;
   else  Fbusy_goal = E3ww * E3hh;
   Fbusy_done = 0;

   do_wthreads(sharp_GR_wthread,NWT);                                            //  worker threads

   Fbusy_goal = 0;
   return 1;
}


//  callable sharp_GR() used by trim/rotate function
//  returns E3 = sharpened E3

void sharp_GR_callable(int amount, int thresh)
{
   using namespace sharpen_names;

   PXM *PXMtemp = E1pxm;                                                         //  save E1
   E1pxm = PXM_copy(E3pxm);                                                      //  copy E3 > E1
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;
   GR_amount = amount;
   GR_thresh = thresh;
   sharp_GR();                                                                   //  E3 = sharpened E1
   PXM_free(E1pxm);
   E1pxm = PXMtemp;                                                              //  restore org. E1
   return;
}


void * sharp_GR_wthread(void *arg)                                               //  worker thread function 
{
   using namespace sharpen_names;

   float       *pix1, *pix3;
   int         ii, px, py, dist = 0;
   int         nc = E1pxm->nc;
   float       amount, thresh;
   float       b1, b1x, b1y, b3x, b3y, b3, bf, f1, f2;
   float       red1, green1, blue1, red3, green3, blue3;

   int         index = *((int *) arg);

   amount = 1 + 0.01 * GR_amount;                                                //  1.0 - 5.0
   thresh = GR_thresh;                                                           //  0 - 100

   for (py = index + 1; py < E3hh; py += NWT)                                    //  loop all image pixels
   for (px = 1; px < E3ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel is outside area
      }

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

      b1 = pixbright(pix1);                                                      //  pixel brightness, 0 - 256
      if (b1 == 0) continue;                                                     //  black, don't change
      b1x = b1 - pixbright(pix1-nc);                                             //  horiz. brightness gradient
      b1y = b1 - pixbright(pix1-nc * E3ww);                                      //  vertical
      f1 = fabsf(b1x + b1y);

      if (f1 < thresh)                                                           //  moderate brightness change for
         f1 = f1 / thresh;                                                       //    pixels below threshold gradient
      else  f1 = 1.0;
      f2 = 1.0 - f1;

      b1x = b1x * amount;                                                        //  amplified gradient
      b1y = b1y * amount;

      b3x = pixbright(pix1-nc) + b1x;                                            //  + prior pixel brightness
      b3y = pixbright(pix1-nc * E3ww) + b1y;                                     //  = new brightness
      b3 = 0.5 * (b3x + b3y);

      b3 = f1 * b3 + f2 * b1;                                                    //  possibly moderated

      bf = b3 / b1;                                                              //  ratio of brightness change
      if (bf < 0) bf = 0;
      if (bf > 4) bf = 4;

      red1 = pix1[0];                                                            //  input RGB
      green1 = pix1[1];
      blue1 = pix1[2];

      red3 = bf * red1;                                                          //  output RGB
      if (red3 > 255.9) red3 = 255.9;
      green3 = bf * green1;
      if (green3 > 255.9) green3 = 255.9;
      blue3 = bf * blue1;
      if (blue3 > 255.9) blue3 = 255.9;

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         red3 = f1 * red3 + f2 * red1;
         green3 = f1 * green3 + f2 * green1;
         blue3 = f1 * blue3 + f2 * blue1;
      }

      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;

      Fbusy_done++;                                                              //  track progress
      if (sharp_cancel) break;
   }

   pthread_exit(0);
}


//  sharpen edges using the Kuwahara algorithm

int sharp_KH()
{
   using namespace sharpen_names;

   void * sharp_KH_wthread(void *arg);
   
   if (sa_stat == 3) Fbusy_goal = sa_Npixel;
   else  Fbusy_goal = E3ww * E3hh;
   Fbusy_done = 0;

   do_wthreads(sharp_KH_wthread,NWT);                                            //  worker threads

   Fbusy_goal = 0;
   return 1;
}


void * sharp_KH_wthread(void *arg)                                               //  worker thread function
{
   using namespace sharpen_names;

   float       *pix1, *pix3;
   int         px, py, qx, qy, rx, ry;
   int         ii, rad, N, dist = 0;
   float       red, green, blue, red2, green2, blue2;
   float       vmin, vall, vred, vgreen, vblue;
   float       red3, green3, blue3;
   float       f1, f2;

   int      index = *((int *) arg);

   rad = KH_radius;                                                              //  user input radius
   N = (rad + 1) * (rad + 1);

   for (py = index + rad; py < E3hh-rad; py += NWT)                              //  loop all image pixels
   for (px = rad; px < E3ww-rad; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel is outside area
      }

      vmin = 99999;
      red3 = green3 = blue3 = 0;

      for (qy = py - rad; qy <= py; qy++)                                        //  loop all surrounding neighborhoods
      for (qx = px - rad; qx <= px; qx++)
      {
         red = green = blue = 0;
         red2 = green2 = blue2 = 0;

         for (ry = qy; ry <= qy + rad; ry++)                                     //  loop all pixels in neighborhood
         for (rx = qx; rx <= qx + rad; rx++)
         {
            pix1 = PXMpix(E1pxm,rx,ry);
            red += pix1[0];                                                      //  compute mean RGB and mean RGB**2
            red2 += pix1[0] * pix1[0];
            green += pix1[1];
            green2 += pix1[1] * pix1[1];
            blue += pix1[2];
            blue2 += pix1[2] * pix1[2];
         }

         red = red / N;                                                          //  mean RGB of neighborhood
         green = green / N;
         blue = blue / N;

         vred = red2 / N - red * red;                                            //  variance RGB
         vgreen = green2 / N - green * green;
         vblue = blue2 / N - blue * blue;

         vall = vred + vgreen + vblue;                                           //  save RGB values with least variance
         if (vall < vmin) {
            vmin = vall;
            red3 = red;
            green3 = green;
            blue3 = blue;
         }
      }

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  if select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         pix1 = PXMpix(E1pxm,px,py);                                             //  input pixel
         red3 = f1 * red3 + f2 * pix1[0];
         green3 = f1 * green3 + f2 * pix1[1];
         blue3 = f1 * blue3 + f2 * pix1[2];
      }

      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;

      Fbusy_done++;                                                              //  track progress
      if (sharp_cancel) break;
   }

   pthread_exit(0);
}


//  sharpen edges using the median difference algorithm

int sharp_MD()
{
   using namespace sharpen_names;

   void * sharp_MD_wthread(void *arg);
   
   int      px, py, ii;
   float    *pix1;
   
   MD_britemap = (int *) zmalloc(E3ww * E3hh * sizeof(int));
   
   for (py = 0; py < E3hh; py++)                                                 //  loop all pixels
   for (px = 0; px < E3ww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                //  initz. pixel brightness map
      ii = py * E3ww + px;
      MD_britemap[ii] = pixbright(pix1);
   }

   if (sa_stat == 3) Fbusy_goal = sa_Npixel;
   else  Fbusy_goal = E3ww * E3hh;
   Fbusy_done = 0;
   
   do_wthreads(sharp_MD_wthread,NWT);

   Fbusy_goal = 0;

   zfree(MD_britemap);
   return 1;
}


void * sharp_MD_wthread(void *arg)                                               //  worker thread function
{
   using namespace sharpen_names;
   
   int         index = *((int *) arg);
   int         rad, dark, light, *britemap;
   int         ii, px, py, dist = 0;
   int         dy, dx, ns;
   float       R, G, B, R2, G2, B2;
   float       F, f1, f2;
   float       *pix1, *pix3;
   int         bright, median;
   int         bsortN[1681];                                                     //  radius <= 20 (41 x 41 pixels)
   
   rad = MD_radius;                                                              //  parameters from dialog
   dark = MD_dark;
   light = MD_light;
   britemap = MD_britemap;

   for (py = index+rad; py < E3hh-rad; py += NWT)                                //  loop all image3 pixels
   for (px = rad; px < E3ww-rad; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pix1 = PXMpix(E1pxm,px,py);                                                //  source pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  target pixel

      ns = 0;

      for (dy = py-rad; dy <= py+rad; dy++)                                      //  loop surrounding pixels
      for (dx = px-rad; dx <= px+rad; dx++)                                      //  get brightness values
      {
         ii = dy * E3ww + dx;
         bsortN[ns] = britemap[ii];
         ns++;
      }

      HeapSort(bsortN,ns);                                                       //  sort the pixels
      median = bsortN[ns/2];                                                     //  median brightness
      
      R = pix3[0];
      G = pix3[1];
      B = pix3[2];
      
      bright = pixbright(pix3);
      
      if (bright < median) {
         F = 1.0 - 0.1 * dark * (median - bright) / (median + 50);
         R2 = R * F;
         G2 = G * F;
         B2 = B * F;
         if (R2 > 0 && G2 > 0 && B2 > 0) {
            R = R2;
            G = G2;
            B = B2;
         }
      }
      
      if (bright > median) {
         F = 1.0 + 0.03 * light * (bright - median) / (median + 50);
         R2 = R * F;
         G2 = G * F;
         B2 = B * F;
         if (R2 < 255 && G2 < 255 && B2 < 255) {
            R = R2;
            G = G2;
            B = B2;
         }
      }

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         R = f1 * R + f2 * pix1[0];
         G = f1 * G + f2 * pix1[1];
         B = f1 * B + f2 * pix1[2];
      }
      
      pix3[0] = R;
      pix3[1] = G;
      pix3[2] = B;

      Fbusy_done++;                                                              //  track progress
   }

   pthread_exit(0);
}


//  Compute the mean brightness of all pixel neighborhoods,
//  using a Gaussian or a flat distribution for the weightings.
//  If a select area is active, only inside pixels are calculated.
//  The flat method is 10-100x faster than the Gaussian method.

void britehood(int radius, char method)
{
   using namespace sharpen_names;

   void * brhood_wthread(void *arg);

   int      rad, radflat2, dx, dy;
   float    kern;

   brhood_radius = radius;
   brhood_method = method;

   if (brhood_method == 'g')                                                     //  compute Gaussian kernel
   {                                                                             //  (not currently used)
      rad = brhood_radius;
      radflat2 = rad * rad;

      for (dy = -rad; dy <= rad; dy++)
      for (dx = -rad; dx <= rad; dx++)
      {
         if (dx*dx + dy*dy <= radflat2)                                          //  cells within radius
            kern = exp( - (dx*dx + dy*dy) / radflat2);
         else kern = 0;                                                          //  outside radius
         brhood_kernel[dy+rad][dx+rad] = kern;
      }
   }

   if (sa_stat == 3) Fbusy_goal = sa_Npixel;                                     //  set up progress tracking
   else Fbusy_goal = E3ww * E3hh;
   Fbusy_done = 0;

   do_wthreads(brhood_wthread,NWT);                                              //  worker threads

   Fbusy_goal = 0;
   return;
}


//  worker thread function

void * brhood_wthread(void *arg)
{
   using namespace sharpen_names;

   int      index = *((int *) arg);
   int      rad = brhood_radius;
   int      ii, px, py, qx, qy, Fstart;
   float    kern, bsum, bsamp, bmean;
   float    *pixel;

   if (brhood_method == 'g')                                                     //  use round gaussian distribution
   {
      for (py = index; py < E3hh; py += NWT)
      for (px = 0; px < E3ww; px++)
      {
         if (sa_stat == 3 && sa_mode != mode_image) {                            //  select area, not whole image
            ii = py * E3ww + px;                                                 //    use only inside pixels
            if (! sa_pixmap[ii]) continue;
         }

         bsum = bsamp = 0;

         for (qy = py-rad; qy <= py+rad; qy++)                                   //  computed weighted sum of brightness
         for (qx = px-rad; qx <= px+rad; qx++)                                   //    for pixels in neighborhood
         {
            if (qy < 0 || qy > E3hh-1) continue;
            if (qx < 0 || qx > E3ww-1) continue;
            kern = brhood_kernel[qy+rad-py][qx+rad-px];
            pixel = PXMpix(E1pxm,qx,qy);
            bsum += pixbright(pixel) * kern;                                     //  sum brightness * weight
            bsamp += kern;                                                       //  sum weights
         }

         bmean = bsum / bsamp;                                                   //  mean brightness
         ii = py * E3ww + px;
         brhood_brightness[ii] = bmean;                                          //  pixel value

         Fbusy_done++;                                                           //  track progress
         if (sharp_cancel) break;
      }
   }

   if (brhood_method == 'f')                                                     //  use square flat distribution
   {
      Fstart = 1;
      bsum = bsamp = 0;

      for (py = index; py < E3hh; py += NWT)
      for (px = 0; px < E3ww; px++)
      {
         if (sa_stat == 3 && sa_mode != mode_image) {                            //  select area, not whole image
            ii = py * E3ww + px;                                                 //     compute only inside pixels
            if (! sa_pixmap[ii]) {
               Fstart = 1;
               continue;
            }
         }

         if (px == 0) Fstart = 1;

         if (Fstart)
         {
            Fstart = 0;
            bsum = bsamp = 0;

            for (qy = py-rad; qy <= py+rad; qy++)                                //  add up all columns
            for (qx = px-rad; qx <= px+rad; qx++)
            {
               if (qy < 0 || qy > E3hh-1) continue;
               if (qx < 0 || qx > E3ww-1) continue;
               pixel = PXMpix(E1pxm,qx,qy);
               bsum += pixbright(pixel);
               bsamp += 1;
            }
         }
         else
         {
            qx = px-rad-1;                                                       //  subtract first-1 column
            if (qx >= 0) {
               for (qy = py-rad; qy <= py+rad; qy++)
               {
                  if (qy < 0 || qy > E3hh-1) continue;
                  pixel = PXMpix(E1pxm,qx,qy);
                  bsum -= pixbright(pixel);
                  bsamp -= 1;
               }
            }
            qx = px+rad;                                                         //  add last column
            if (qx < E3ww) {
               for (qy = py-rad; qy <= py+rad; qy++)
               {
                  if (qy < 0 || qy > E3hh-1) continue;
                  pixel = PXMpix(E1pxm,qx,qy);
                  bsum += pixbright(pixel);
                  bsamp += 1;
               }
            }
         }

         bmean = bsum / bsamp;                                                   //  mean brightness
         ii = py * E3ww + px;
         brhood_brightness[ii] = bmean;

         Fbusy_done++;                                                           //  track progress
         if (sharp_cancel) break;
      }
   }

   pthread_exit(0);
}


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

//  image blur function

namespace blur_names
{
   int         Fnormblur;                                                        //  normal blur
   float       Nblur_radius;                                                     //  blur radius
   float       blur_weight[1415];                                                //  blur radius limit 999 

   int         Fradblur;                                                         //  radial blur
   int         RBlen;                                                            //  radial blur length
   int         Cx, Cy;                                                           //  image center of radial blur
   
   int         Fdirblur;                                                         //  directed blur
   float       Dmdx, Dmdy, Dmdw, Dmdh;
   float       DD, Dspan, Dintens;
   
   int         Fgradblur;                                                        //  graduated blur
   float       gblur_radius;                                                     //  blur radius
   int         con_limit;                                                        //  contrast limit
   uint8       *pixcon;
   int         pixseq_done[122][122];                                            //  up to gblur_radius = 60
   int         pixseq_angle[1000];
   int         pixseq_dx[13000], pixseq_dy[13000];
   int         pixseq_rad[13000];
   int         max1 = 999, max2 = 12999;                                         //  for later overflow check
   
   int         Fpaintblur;                                                       //  paint blur
   int         pmode = 1;                                                        //  1/2 = blend/restore
   int         pblur_radius = 20;                                                //  mouse radius
   float       powcent, powedge;                                                 //  power at center and edge
   float       kernel[402][402];                                                 //  radius limit 200
   int         mousex, mousey;                                                   //  mouse click/drag position
   
   int         Fblurbackground;                                                  //  blur background via select area

   editfunc    EFblur;
   VOL int     Fcancel;
   PXM         *E2pxm;
   int         E3ww, E3hh;                                                       //  image dimensions
}


//  menu function

void m_blur(GtkWidget *, cchar *menu)                                            //  consolidate all blur functions
{
   using namespace blur_names;

   int    blur_dialog_event(zdialog *zd, cchar *event);
   void   blur_mousefunc();
   void * blur_thread(void *);

   cchar  *radblur_tip = E2X("Click to set center");
   cchar  *dirblur_tip = E2X("Pull image using the mouse");
   cchar  *paintblur_tip = E2X("left drag: blend image \n"
                               "right drag: restore image");

   F1_help_topic = "blur";

   EFblur.menuname = menu;
   EFblur.menufunc = m_blur;
   EFblur.funcname = "blur";
   EFblur.Farea = 2;                                                             //  select area usable
   EFblur.threadfunc = blur_thread;                                              //  thread function
   EFblur.mousefunc = blur_mousefunc;
   EFblur.Frestart = 1;                                                          //  allow restart
   if (! edit_setup(EFblur)) return;                                             //  setup edit

/***
          _________________________________________
         |             Blur Image                  |
         |                                         |
         |  [x] Normal Blur   Radius [____]        |
         |  - - - - - - - - - - - - - - - - - -    |
         |  [x] Radial Blur                        |
         |  Length [___]  Center X [___] Y [___]   |
         |  - - - - - - - - - - - - - - - - - -    |
         |  [X] Directed Blur                      |
         |  Blur Span  [___]  Intensity   [___]    |
         |  - - - - - - - - - - - - - - - - - -    |
         |  [X] Graduated Blur                     |
         |  Radius [___]  Contrast Limit [___]     |
         |  - - - - - - - - - - - - - - - - - -    |
         |  [X] Paint Blur                         |
         |  Radius [___]  Power [___]  Edge [___]  |
         |  - - - - - - - - - - - - - - - - - -    |
         |  [X] Blur Background                    |
         |                                         |
         |         [Reset] [Apply] [Done] [Cancel] |
         |_________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Blur Radius"),Mwin,Breset,Bapply,Bdone,Bcancel,null);
   EFblur.zd = zd;

   zdialog_add_widget(zd,"hbox","hbnb","dialog");
   zdialog_add_widget(zd,"check","Fnormblur","hbnb",E2X("Normal Blur"),"space=2");
   zdialog_add_widget(zd,"label","space","hbnb",0,"space=5");
   zdialog_add_widget(zd,"label","labrad","hbnb",Bradius,"space=5");
   zdialog_add_widget(zd,"zspin","Nblur_radius","hbnb","1|999|1|10","space=5|size=3");

   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbrb1","dialog");
   zdialog_add_widget(zd,"check","Fradblur","hbrb1",E2X("Radial Blur"),"space=2");
   zdialog_add_widget(zd,"hbox","hbrb2","dialog");
   zdialog_add_widget(zd,"label","labrbl","hbrb2",Blength,"space=5");
   zdialog_add_widget(zd,"zspin","RBlen","hbrb2","1|999|1|100","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbrb2",0,"space=5");
   zdialog_add_widget(zd,"label","labc","hbrb2",Bcenter,"space=3");
   zdialog_add_widget(zd,"label","labcx","hbrb2","X","space=3");
   zdialog_add_widget(zd,"zentry","Cx","hbrb2",0,"space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbrb2",0,"space=3");
   zdialog_add_widget(zd,"label","labcy","hbrb2","Y","space=3");
   zdialog_add_widget(zd,"zentry","Cy","hbrb2",0,"space=3|size=3");

   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbdb1","dialog");
   zdialog_add_widget(zd,"check","Fdirblur","hbdb1",E2X("Directed Blur"),"space=2");
   zdialog_add_widget(zd,"hbox","hbdb2","dialog");
   zdialog_add_widget(zd,"label","labspan","hbdb2",E2X("Blur Span"),"space=5");
   zdialog_add_widget(zd,"zspin","span","hbdb2","0.00|1.0|0.01|0.1","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbdb2",0,"space=5");
   zdialog_add_widget(zd,"label","labint","hbdb2",E2X("Intensity"));
   zdialog_add_widget(zd,"zspin","intens","hbdb2","0.00|1.0|0.01|0.2","space=3|size=3");

   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbgb1","dialog");
   zdialog_add_widget(zd,"check","Fgradblur","hbgb1",E2X("Graduated Blur"),"space=2");
   zdialog_add_widget(zd,"hbox","hbgb2","dialog");
   zdialog_add_widget(zd,"label","labgrad","hbgb2",Bradius,"space=5");
   zdialog_add_widget(zd,"zspin","gblur_radius","hbgb2","1|50|1|10","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbgb2",0,"space=5");
   zdialog_add_widget(zd,"label","lablim","hbgb2",E2X("Contrast Limit"));
   zdialog_add_widget(zd,"zspin","con_limit","hbgb2","1|255|1|50","space=3|size=3");

   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbpb1","dialog");
   zdialog_add_widget(zd,"check","Fpaintblur","hbpb1",E2X("Paint Blur"),"space=2");
   zdialog_add_widget(zd,"hbox","hbpb2","dialog");
   zdialog_add_widget(zd,"label","labpaint","hbpb2",Bradius,"space=5");
   zdialog_add_widget(zd,"zspin","pblur_radius","hbpb2","2|200|1|20","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbpb2",0,"space=5");
   zdialog_add_widget(zd,"label","labpow","hbpb2",E2X("Power"));
   zdialog_add_widget(zd,"zspin","powcent","hbpb2","0|100|1|30","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbpb2",0,"space=5");
   zdialog_add_widget(zd,"label","labedge","hbpb2",Bedge);
   zdialog_add_widget(zd,"zspin","powedge","hbpb2","0|100|1|10","space=3|size=3");
   
   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbbg","dialog");
   zdialog_add_widget(zd,"check","Fblurbackground","hbbg",E2X("Blur Background"),"space=2");

   zdialog_add_ttip(zd,"Fradblur",radblur_tip);
   zdialog_add_ttip(zd,"Fdirblur",dirblur_tip);
   zdialog_add_ttip(zd,"Fpaintblur",paintblur_tip);
   
   E3ww = E3pxm->ww;                                                             //  image dimensions
   E3hh = E3pxm->hh;

   Fcancel = 0;                                                                  //  initial status
   E2pxm = 0;

   Fnormblur = 0;                                                                //  default settings
   Nblur_radius = 10;

   Fradblur = 0;
   RBlen = 100;
   Cx = E3ww / 2;
   Cy = E3hh / 2;
   zdialog_stuff(zd,"Cx",Cx);
   zdialog_stuff(zd,"Cy",Cy);

   Fdirblur = 0;
   Dspan = 0.1;
   Dintens = 0.2;

   Fgradblur = 0;
   con_limit = 1;
   gblur_radius = 10;
   
   Fpaintblur = 0;
   pmode = 1;
   pblur_radius = 20;
   powcent = 30;
   powedge = 10;
   
   zdialog_restore_inputs(zd);

   zdialog_stuff(zd,"Fblurbackground",0);                                        //  no blur background at start
   Fblurbackground = 0;

   zdialog_run(zd,blur_dialog_event,"save");                                     //  run dialog - parallel
   zdialog_send_event(zd,"pblur_radius");                                        //  get kernel initialized
   return;
}


//  dialog event and completion callback function

int blur_dialog_event(zdialog * zd, cchar *event)
{
   using namespace blur_names;

   void   blur_mousefunc();

   float    frad, kern;
   int      rad, dx, dy;

   if (strmatch(event,"done")) zd->zstat = 3;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  from f_open()
   
   if (zstrstr("Fnormblur Fradblur Fdirblur Fgradblur "                          //  checkboxes work like radio buttons
              "Fpaintblur Fblurbackground",event)) {
      zdialog_stuff(zd,"Fnormblur",0);
      zdialog_stuff(zd,"Fradblur",0);
      zdialog_stuff(zd,"Fdirblur",0);
      zdialog_stuff(zd,"Fgradblur",0);
      zdialog_stuff(zd,"Fpaintblur",0);
      zdialog_stuff(zd,"Fblurbackground",0);
      zdialog_stuff(zd,event,1);
      zdialog_fetch(zd,"Fnormblur",Fnormblur);
      zdialog_fetch(zd,"Fradblur",Fradblur);
      zdialog_fetch(zd,"Fdirblur",Fdirblur);
      zdialog_fetch(zd,"Fgradblur",Fgradblur);
      zdialog_fetch(zd,"Fpaintblur",Fpaintblur);
      zdialog_fetch(zd,"Fblurbackground",Fblurbackground);
   }
   
   if (Fradblur || Fdirblur)                                                     //  connect mouse
      takeMouse(blur_mousefunc,dragcursor);
   else if (Fpaintblur)
      takeMouse(blur_mousefunc,0);
   else freeMouse();

   zdialog_fetch(zd,"Fnormblur",Fnormblur);                                      //  get all dialog inputs
   zdialog_fetch(zd,"Nblur_radius",Nblur_radius);

   zdialog_fetch(zd,"Fradblur",Fradblur);
   zdialog_fetch(zd,"RBlen",RBlen);
   zdialog_fetch(zd,"Cx",Cx);
   zdialog_fetch(zd,"Cy",Cy);

   zdialog_fetch(zd,"Fdirblur",Fdirblur);
   zdialog_fetch(zd,"span",Dspan);
   zdialog_fetch(zd,"intens",Dintens);

   zdialog_fetch(zd,"Fgradblur",Fgradblur);
   zdialog_fetch(zd,"gblur_radius",gblur_radius);
   zdialog_fetch(zd,"con_limit",con_limit);

   zdialog_fetch(zd,"Fpaintblur",Fpaintblur);
   zdialog_fetch(zd,"pblur_radius",pblur_radius);
   zdialog_fetch(zd,"powcent",powcent);
   zdialog_fetch(zd,"powedge",powedge);
   
   zdialog_fetch(zd,"Fblurbackground",Fblurbackground);

   if (Fblurbackground) {                                                        //  cancel and start new function
      Fcancel = 1;
      edit_cancel(0);
      if (E2pxm) PXM_free(E2pxm);
      E2pxm = 0;
      m_blur_background(0,0);
      return 1;                                                                  //  19.0
   }

   if (zstrstr("pblur_radius powcent powedge",event))                            //  paint blur parameters
   {
      zdialog_fetch(zd,"pblur_radius",pblur_radius);                             //  mouse radius
      zdialog_fetch(zd,"powcent",powcent);                                       //  center transparency
      zdialog_fetch(zd,"powedge",powedge);                                       //  powedge transparency

      powcent = 0.01 * powcent;                                                  //  scale 0 ... 1
      powedge = 0.01 * powedge;
      rad = pblur_radius;

      for (dy = -rad; dy <= rad; dy++)                                           //  build kernel
      for (dx = -rad; dx <= rad; dx++)
      {
         frad = sqrt(dx*dx + dy*dy);
         kern = (rad - frad) / rad;                                              //  center ... powedge  >>  1 ... 0
         kern = kern * (powcent - powedge) + powedge;                            //  strength  center ... powedge
         if (kern < 0) kern = 0;
         if (kern > 1) kern = 1;
         if (frad > rad) kern = 2;                                               //  beyond radius, within square
         kernel[dx+rad][dy+rad] = kern;
      }
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [reset]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
      }

      else if (zd->zstat == 2)                                                   //  [apply]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         signal_thread();                                                        //  trigger thread
         return 1;                                                               //  do not free E2                     19.0
      }

      else if (zd->zstat == 3)                                                   //  [done]
         edit_done(0);

      else {                                                                     //  [cancel]
         Fcancel = 1;
         edit_cancel(0);                                                         //  discard edit
      }

      if (E2pxm) PXM_free(E2pxm);                                                //  free memory
      E2pxm = 0;
   }

   return 1;
}


//  blur mouse function

void blur_mousefunc()                                                            //  mouse function
{
   using namespace blur_names;
   
   if (! CEF) return;
   
   if (Fnormblur) 
   {
      freeMouse();
      return;
   }

   if (Fradblur && LMclick)                                                      //  radial blur, new center
   {
      zdialog *zd = CEF->zd;
      Cx = Mxposn;
      Cy = Myposn;
      zdialog_stuff(zd,"Cx",Cx);
      zdialog_stuff(zd,"Cy",Cy);
      LMclick = 0;
      signal_thread();
   }

   if (Fdirblur && (Mxdrag || Mydrag))                                           //  directed blur, mouse drag
   {
      Dmdx = Mxdown;                                                             //  drag origin
      Dmdy = Mydown;
      Dmdw = Mxdrag - Mxdown;                                                    //  drag increment
      Dmdh = Mydrag - Mydown;
      Mxdrag = Mydrag = 0;
      signal_thread();
   }
   
   if (Fpaintblur)                                                               //  paint blur
   {
      int      px, py, rr;

      if (LMclick || RMclick)                                                    //  mouse click
      {
         if (LMclick) pmode = 1;                                                 //  left click, paint
         if (RMclick) pmode = 2;                                                 //  right click, erase
         mousex = Mxclick;
         mousey = Myclick;
         signal_thread();
      }

      else if (Mxdrag || Mydrag)                                                 //  mouse drag in progress
      {
         if (Mbutton == 1) pmode = 1;                                            //  left drag, paint
         if (Mbutton == 3) pmode = 2;                                            //  right drag, erase
         mousex = Mxdrag;
         mousey = Mydrag;
         signal_thread();
      }

      cairo_t *cr = draw_context_create(gdkwin,draw_context);

      px = mousex - pblur_radius - 1;                                            //  repaint modified area
      py = mousey - pblur_radius - 1;
      rr = 2 * pblur_radius + 3;
      Fpaint3(px,py,rr,rr,cr);

      draw_mousecircle(Mxposn,Myposn,pblur_radius,0,cr);                         //  redraw mouse circle
      draw_context_destroy(draw_context);

      LMclick = RMclick = Mxdrag = Mydrag = 0;                                   //  reset mouse
   }

   return;
}


//  image blur thread function

void * blur_thread(void *)
{
   using namespace blur_names;

   void * normblur_wthread(void *);
   void * radblur_wthread(void *);
   void * dirblur_wthread(void *);
   void * gradblur_wthread(void *);
   void * paintblur_wthread(void *);

   int         ii, jj;
   float       rad, w, wsum;
   float       dd, d1, d2, d3, d4;
   int         px, py, dx, dy, adx, ady;
   float       *pix1, *pix2;
   float       contrast, maxcon;
   float       rad1, rad2, angle, astep;

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      if (Fnormblur)                                                             //  normal blur
      {
         if (E2pxm) PXM_free(E2pxm);
         E2pxm = PXM_copy(E1pxm);                                                //  intermediate image

         rad = Nblur_radius;
         wsum = 0;
         
         for (ii = 0; ii < rad; ii++)                                            //  set pixel weight per distance
         {                                                                       //      example, rad = 10
            w = 1.0 - ii / rad;                                                  //  dist:   0   1   2   3   5   7   9
            w = w * w;                                                           //  weight: 1  .81 .64 .49 .25 .09 .01
            blur_weight[ii] = w;
            wsum += w;
         }
         
         for (ii = 0; ii < rad; ii++)                                            //  make weights sum to 1.0
            blur_weight[ii] = blur_weight[ii] / wsum;

         if (sa_stat == 3) Fbusy_goal = sa_Npixel;                               //  setup progress counter
         else  Fbusy_goal = E3ww * E3hh;
         Fbusy_goal = 2 * Fbusy_goal;
         Fbusy_done = 0;

         do_wthreads(normblur_wthread,NWT);                                      //  worker threads
      }

      if (Fradblur)                                                              //  radial blur
      {
         if (E2pxm) PXM_free(E2pxm);

         if (Fnormblur)                                                          //  if normal blur done before,
            E2pxm = PXM_copy(E3pxm);                                             //    use the blur output image
         else E2pxm = PXM_copy(E1pxm);                                           //  else use the original image

         if (sa_stat == 3) Fbusy_goal = sa_Npixel;                               //  setup progress counter
         else  Fbusy_goal = E3ww * E3hh;
         Fbusy_done = 0;

         do_wthreads(radblur_wthread,NWT);                                       //  worker threads
      }
      
      if (Fdirblur)                                                              //  directed blur
      {
         d1 = (Dmdx-0) * (Dmdx-0) + (Dmdy-0) * (Dmdy-0);                         //  distance, mouse to 4 corners
         d2 = (E3ww-Dmdx) * (E3ww-Dmdx) + (Dmdy-0) * (Dmdy-0);
         d3 = (E3ww-Dmdx) * (E3ww-Dmdx) + (E3hh-Dmdy) * (E3hh-Dmdy);
         d4 = (Dmdx-0) * (Dmdx-0) + (E3hh-Dmdy) * (E3hh-Dmdy);

         dd = d1;
         if (d2 > dd) dd = d2;                                                   //  find greatest corner distance
         if (d3 > dd) dd = d3;
         if (d4 > dd) dd = d4;

         DD = dd * 0.5 * Dspan;

         do_wthreads(dirblur_wthread,NWT);                                       //  worker threads
      }
      
      if (Fgradblur)                                                             //  graduated blur
      {
         pixcon = (uint8 *) zmalloc(E3ww * E3hh);                                //  pixel contrast map

         for (py = 1; py < E3hh-1; py++)                                         //  loop interior pixels
         for (px = 1; px < E3ww-1; px++)
         {
            pix1 = PXMpix(E1pxm,px,py);                                          //  this pixel in base image E1
            contrast = maxcon = 0.0;

            for (dx = px-1; dx <= px+1; dx++)                                    //  loop neighbor pixels
            for (dy = py-1; dy <= py+1; dy++)
            {
               pix2 = PXMpix(E1pxm,dx,dy);
               contrast = 1.0 - PIXMATCH(pix1,pix2);                             //  contrast, 0-1
               if (contrast > maxcon) maxcon = contrast;
            }

            ii = py * E3ww + px;                                                 //  ii maps to (px,py)
            pixcon[ii] = 255 * maxcon;                                           //  pixel contrast, 0 to 255
         }

         rad1 = gblur_radius;

         for (dy = 0; dy <= 2*rad1; dy++)                                        //  no pixels mapped yet
         for (dx = 0; dx <= 2*rad1; dx++)
            pixseq_done[dx][dy] = 0;

         ii = jj = 0;

         astep = 0.5 / rad1;                                                     //  0.5 pixel steps at rad1 from center

         for (angle = 0; angle < 2*PI; angle += astep)                           //  loop full circle
         {
            pixseq_angle[ii] = jj;                                               //  start pixel sequence for this angle
            ii++;

            for (rad2 = 1; rad2 <= rad1; rad2++)                                 //  loop rad2 from center to edge
            {
               dx = lround(rad2 * cos(angle));                                   //  pixel at angle and rad2
               dy = lround(rad2 * sin(angle));
               adx = rad1 + dx;
               ady = rad1 + dy;
               if (pixseq_done[adx][ady]) continue;                              //  pixel already mapped
               pixseq_done[adx][ady] = 1;                                        //  map pixel
               pixseq_dx[jj] = dx;                                               //  save pixel sequence for angle
               pixseq_dy[jj] = dy;
               pixseq_rad[jj] = rad2;                                            //  pixel radius
               jj++;
            }
            pixseq_rad[jj] = 9999;                                               //  mark end of pixels for angle
            jj++;
         }

         pixseq_angle[ii] = 9999;                                                //  mark end of angle steps

         if (ii > max1 || jj > max2)                                             //  should not happen
            zappcrash("gradblur array overflow");

         if (sa_stat == 3) Fbusy_goal = sa_Npixel;                               //  setup progress tracking
         else  Fbusy_goal = E3ww * E3hh;
         Fbusy_done = 0;

         do_wthreads(gradblur_wthread,NWT);                                      //  worker threads

         zfree(pixcon);
      }
      
      if (Fpaintblur)
         do_wthreads(paintblur_wthread,NWT);                                     //  worker threads
      
      Fbusy_goal = 0;
      CEF->Fmods++;
      CEF->Fsaved = 0;

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();
   }
   
   return 0;                                                                     //  not executed, stop g++ warning
}


//  normal blur worker thread

void * normblur_wthread(void *arg)
{
   using namespace blur_names;

   int      index = *((int *) arg);
   int      rad = Nblur_radius;
   int      ii, dist = 0;
   int      px, py, qx, qy;
   int      ylo, yhi, xlo, xhi;
   float    R, G, B, w1, w2, f1, f2;
   float    *pix1, *pix3, *pix2;

   for (py = index; py < E3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < E3ww; px++)
   {
      if (Fcancel) pthread_exit(0);                                              //  user cancel

      ii = py * E3ww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pix1 = PXMpix(E1pxm,px,py);                                                //  source pixel
      pix2 = PXMpix(E2pxm,px,py);                                                //  target pixel - intermediate image
      
      ylo = py - rad;
      if (ylo < 0) ylo = 0;
      yhi = py + rad;
      if (yhi > E3hh-1) yhi = E3hh - 1;
      
      R = G = B = 0;
      w2 = 0;

      for (qy = ylo; qy <= yhi; qy++)                                            //  loop pixels in same column
      {
         if (sa_stat == 3) {  
            ii = qy * E3ww + px;                                                 //  don't use pixels outside area   
            dist = sa_pixmap[ii];
            if (! dist) continue;
         }
         
         w1 = blur_weight[abs(qy-py)];                                           //  weight based on radius
         pix1 = PXMpix(E1pxm,px,qy);
         R += w1 * pix1[0];                                                      //  accumulate RGB * weight
         G += w1 * pix1[1];
         B += w1 * pix1[2];
         w2 += w1;                                                               //  accumulate weights
      }
      
      R = R / w2;                                                                //  normalize
      G = G / w2;
      B = B / w2;

      pix2[0] = R;                                                               //  weighted average
      pix2[1] = G;
      pix2[2] = B;

      Fbusy_done++;                                                              //  track progress
   }

   for (py = index; py < E3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < E3ww; px++)
   {
      if (Fcancel) pthread_exit(0);                                              //  user cancel

      ii = py * E3ww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pix2 = PXMpix(E2pxm,px,py);                                                //  source pixel - intermediate image
      pix3 = PXMpix(E3pxm,px,py);                                                //  target pixel - final image
      
      xlo = px - rad;
      if (xlo < 0) xlo = 0;
      xhi = px + rad;
      if (xhi > E3ww-1) xhi = E3ww - 1;
      
      R = G = B = 0;
      w2 = 0;

      for (qx = xlo; qx <= xhi; qx++)                                            //  loop pixels in same row
      {
         if (sa_stat == 3) {  
            ii = py * E3ww + qx;                                                 //  don't use pixels outside area   
            dist = sa_pixmap[ii];
            if (! dist) continue;
         }
         
         w1 = blur_weight[abs(qx-px)];                                           //  weight based on radius
         pix2 = PXMpix(E2pxm,qx,py);
         R += w1 * pix2[0];                                                      //  accumulate RGB * weight
         G += w1 * pix2[1];
         B += w1 * pix2[2];
         w2 += w1;                                                               //  accumulate weights
      }
      
      R = R / w2;                                                                //  normalize
      G = G / w2;
      B = B / w2;

      pix3[0] = R;                                                               //  weighted average
      pix3[1] = G;
      pix3[2] = B;

      Fbusy_done++;                                                              //  track progress
   }

   if (sa_stat == 3 && sa_blendwidth > 0)                                        //  select area has edge blend
   {
      for (py = index; py < E3hh; py += NWT)                                     //  loop all image pixels
      for (px = 0; px < E3ww; px++)
      {
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                   //  omit pixels outside area
         if (dist >= sa_blendwidth) continue;                                    //  omit if > blendwidth from edge

         pix1 = PXMpix(E1pxm,px,py);                                             //  source pixel
         pix3 = PXMpix(E3pxm,px,py);                                             //  target pixel
         f1 = sa_blendfunc(dist);                                                //  blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         pix3[0] = f1 * pix3[0] + f2 * pix1[0];
         pix3[1] = f1 * pix3[1] + f2 * pix1[1];
         pix3[2] = f1 * pix3[2] + f2 * pix1[2];
      }
   }

   pthread_exit(0);
}


//  radial blur worker thread

void * radblur_wthread(void *arg)
{
   using namespace blur_names;

   int      index = *((int *) arg);
   int      ii, dist = 0;
   int      px, py, qx, qy, qz;
   float    *pix2, *pix3;
   float    R, Rx, Ry, Rz;
   float    f1, f2;
   int      Rsum, Gsum, Bsum, Npix;

   for (py = index+1; py < E3hh-1; py += NWT)                                    //  loop all image pixels
   for (px = 1; px < E3ww-1; px++)
   {
      if (Fcancel) pthread_exit(0);                                              //  user cancel

      ii = py * E3ww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }
      
      Rsum = Gsum = Bsum = Npix = 0;                                             //  reset RGB sums
      
      R = sqrtf((px-Cx)*(px-Cx) + (py-Cy)*(py-Cy));                              //  distance (Cx,Cy) to (px,py)
      if (R == 0) continue;
      Rx = (px-Cx)/R;                                                            //  unit vector along (Cx,Cy) to (px,py)
      Ry = (py-Cy)/R;
      if (fabsf(Rx) > fabsf(Ry)) Rz = 1.0 / fabsf(Rx);                           //  Rz is 1.0 .. 1.414
      else Rz = 1.0 / fabsf(Ry);
      Rx = Rx * Rz;                                                              //  vector with max x/y component = 1
      Ry = Ry * Rz;
      
      for (qz = 0; qz < RBlen; qz++)                                             //  loop (qx,qy) from (px,py) 
      {                                                                          //    in direction to (Cx,Cy)
         qx = px - qz * Rx;                                                      //      for distance RBlen
         qy = py - qz * Ry;
         if (qx < 0 || qx > E3ww-1) break;
         if (qy < 0 || qy > E3hh-1) break;

         if (sa_stat == 3) {  
            ii = qy * E3ww + qx;                                                 //  don't use pixels outside area   
            dist = sa_pixmap[ii];
            if (! dist) continue;
         }

         pix2 = PXMpix(E2pxm,qx,qy);                                             //  sum RGB values
         Rsum += pix2[0];
         Gsum += pix2[1];
         Bsum += pix2[2];
         Npix++;                                                                 //  count pixels in sum
      }
      
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel is average of 
      pix3[0] = Rsum / Npix;                                                     //   pixels in line of length RBlen
      pix3[1] = Gsum / Npix;                                                     //    from (px,py) to (Cx,Cy)
      pix3[2] = Bsum / Npix;

      Fbusy_done++;                                                              //  track progress
   }

   if (sa_stat == 3 && sa_blendwidth > 0)                                        //  select area has edge blend
   {
      for (py = index; py < E3hh; py += NWT)                                     //  loop all image pixels
      for (px = 0; px < E3ww; px++)
      {
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                   //  omit pixels outside area
         if (dist >= sa_blendwidth) continue;                                    //  omit if > blendwidth from edge

         pix2 = PXMpix(E2pxm,px,py);                                             //  source pixel
         pix3 = PXMpix(E3pxm,px,py);                                             //  target pixel
         f1 = sa_blendfunc(dist);                                                //  blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         pix3[0] = f1 * pix3[0] + f2 * pix2[0];
         pix3[1] = f1 * pix3[1] + f2 * pix2[1];
         pix3[2] = f1 * pix3[2] + f2 * pix2[2];
      }
   }

   pthread_exit(0);
}


//  directed blur worker thread

void * dirblur_wthread(void *arg)
{
   using namespace blur_names;

   int      index = *((int *) arg);
   int      ii, px, py, dist = 0, vstat;
   float    d, mag, dispx, dispy;
   float    F1, F2, f1, f2;
   float    vpix[4], *pix1, *pix3;

   F1 = Dintens * Dintens;
   F2 = 1.0 - F1;

   for (py = index; py < E3hh; py += NWT)                                        //  process all pixels
   for (px = 0; px < E3ww; px++)
   {
      ii = py * E3ww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
      }

      d = (px-Dmdx)*(px-Dmdx) + (py-Dmdy)*(py-Dmdy);
      mag = (1.0 - d / DD);
      if (mag < 0) continue;

      mag = mag * mag;                                                           //  faster than pow(mag,4);
      mag = mag * mag;

      dispx = -Dmdw * mag;                                                       //  displacement = drag * mag
      dispy = -Dmdh * mag;

      vstat = vpixel(E3pxm,px+dispx,py+dispy,vpix);                              //  input virtual pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      if (vstat) {
         pix3[0] = F2 * pix3[0] + F1 * vpix[0];                                  //  output = input pixel blend
         pix3[1] = F2 * pix3[1] + F1 * vpix[1];
         pix3[2] = F2 * pix3[2] + F1 * vpix[2];
      }

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  within edge blend area
         pix1 = PXMpix(E1pxm,px,py);                                             //  input pixel
         f1 = sa_blendfunc(dist);
         f2 = 1.0 - f1;
         pix3[0] = f1 * pix3[0] + f2 * pix1[0];
         pix3[1] = f1 * pix3[1] + f2 * pix1[1];
         pix3[2] = f1 * pix3[2] + f2 * pix1[2];
      }
   }

   pthread_exit(0);                                                              //  exit thread
}


//  graduated blur worker thread

void * gradblur_wthread(void *arg)
{
   using namespace blur_names;

   int      index = *((int *) arg);
   int      ii, jj, npix, dist = 0;
   int      px, py, dx, dy;
   float    red, green, blue, f1, f2;
   float    *pix1, *pix3, *pixN;
   int      rad, blurrad, con;
   
   for (py = index+1; py < E3hh-1; py += NWT)                                    //  loop interior pixels
   for (px = 1; px < E3ww-1; px++)
   {
      if (Fcancel) pthread_exit(0);

      ii = py * E3ww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
      }

      if (pixcon[ii] > con_limit) continue;                                      //  high contrast pixel

      pix1 = PXMpix(E1pxm,px,py);                                                //  source pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  target pixel

      red = pix1[0];                                                             //  blur center pixel
      green = pix1[1];
      blue = pix1[2];
      npix = 1;

      blurrad = 1.0 + gblur_radius * (con_limit - pixcon[ii]) / con_limit;       //  blur radius for pixel, 1 - gblur_radius

      for (ii = 0; ii < 2000; ii++)                                              //  loop angle around center pixel
      {
         jj = pixseq_angle[ii];                                                  //  1st pixel for angle step ii
         if (jj == 9999) break;                                                  //  none, end of angle loop

         while (true)                                                            //  loop pixels from center to radius
         {
            rad = pixseq_rad[jj];                                                //  next pixel step radius
            if (rad > blurrad) break;                                            //  stop here if beyond blur radius
            dx = pixseq_dx[jj];                                                  //  next pixel step px/py
            dy = pixseq_dy[jj];
            if (px+dx < 0 || px+dx > E3ww-1) break;                              //  stop here if off the edge
            if (py+dy < 0 || py+dy > E3hh-1) break;

            pixN = PXMpix(E1pxm,px+dx,py+dy);
            con = 255 * (1.0 - PIXMATCH(pix1,pixN));
            if (con > con_limit) break;

            red += pixN[0];                                                      //  add pixel RGB values
            green += pixN[1];
            blue += pixN[2];
            npix++;
            jj++;
         }
      }

      pix3[0] = red / npix;                                                      //  average pixel values
      pix3[1] = green / npix;
      pix3[2] = blue / npix;
      
      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  within edge blend area
         f1 = sa_blendfunc(dist);
         f2 = 1.0 - f1;
         pix3[0] = f1 * pix3[0] + f2 * pix1[0];
         pix3[1] = f1 * pix3[1] + f2 * pix1[1];
         pix3[2] = f1 * pix3[2] + f2 * pix1[2];
      }

      Fbusy_done++;                                                              //  track progress
   }

   pthread_exit(0);
}


//  paint blur worker thread

void * paintblur_wthread(void *arg)
{
   using namespace blur_names;
   
   int         index = *((int *) arg);
   float       *pix1, *pix3, *pixm;
   int         radius, radius2, npix;
   int         px, py, dx, dy, qx, qy, rx, ry, sx, sy;
   int         ii, dist = 0;
   float       kern, kern2, meanR, meanG, meanB;

   px = mousex;
   py = mousey;
   radius = pblur_radius;

   for (dy = -radius+index; dy <= radius; dy += NWT)                             //  loop within mouse radius
   for (dx = -radius; dx <= radius; dx++)
   {
      qx = px + dx;
      qy = py + dy;

      if (qx < 0 || qx > E3ww-1) continue;                                       //  off image
      if (qy < 0 || qy > E3hh-1) continue;

      if (sa_stat == 3) {                                                        //  select area active
         ii = qy * E3ww + qx;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                   //  pixel is outside area
      }

      kern = kernel[dx+radius][dy+radius];                                       //  mouse transparencies
      if (kern > 1) continue;                                                    //  outside mouse radius

      if (sa_stat == 3 && dist < sa_blendwidth)                                  //  within blend distance
         kern = kern * sa_blendfunc(dist);
         
      pix1 = PXMpix(E1pxm,qx,qy);                                                //  original pixel
      pix3 = PXMpix(E3pxm,qx,qy);                                                //  edited pixel
      
      meanR = meanG = meanB = npix = 0;
      radius2 = sqrtf(radius);                                                   //  radius = 2..99  >>  radius2 = 1..9
      
      for (ry = -radius2; ry <= radius2; ry++)
      for (rx = -radius2; rx <= radius2; rx++)
      {
         sx = qx + rx;
         sy = qy + ry;
         
         if (px - sx < -radius || px - sx > radius) continue;                    //  outside mouse radius
         if (py - sy < -radius || py - sy > radius) continue;

         if (sx < 0 || sx > E3ww-1) continue;                                    //  off image
         if (sy < 0 || sy > E3hh-1) continue;
         
         pixm = PXMpix(E3pxm,sx,sy);
         meanR += pixm[0];         
         meanG += pixm[1];         
         meanB += pixm[2];         
         npix++;
      }
      
      if (npix == 0) continue;
      
      meanR = meanR / npix;
      meanG = meanG / npix;
      meanB = meanB / npix;
      
      if (pmode == 1) {                                                          //  blend
         kern2 = 0.5 * kern;
         pix3[0] = kern2 * meanR + (1.0 - kern2) * pix3[0];                      //  pix3 tends to regional mean
         pix3[1] = kern2 * meanG + (1.0 - kern2) * pix3[1];
         pix3[2] = kern2 * meanB + (1.0 - kern2) * pix3[2];
      }

      if (pmode == 2) {                                                          //  restore
         kern2 = 0.1 * kern;
         pix3[0] = kern2 * pix1[0] + (1.0 - kern2) * pix3[0];                    //  pix3 tends to pix1
         pix3[1] = kern2 * pix1[1] + (1.0 - kern2) * pix3[1];
         pix3[2] = kern2 * pix1[2] + (1.0 - kern2) * pix3[2];
      }
   }

   pthread_exit(0);
}


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

//  Blur Background
//  Blur the image outside of a selected area or areas. 
//  Blur increases with distance from selected area edges.

namespace blur_BG_names 
{
   int         conrad, incrad;               //  constant or increasing blur
   int         conbrad;                      //  constant blur radius
   int         minbrad;                      //  min. blur radius
   int         maxbrad;                      //  max. blur radius
   VOL int     Fcancel;                      //  GCC inconsistent
   int         E3ww, E3hh;                   //  image dimensions
   int         maxdist;                      //  max. area edge distance
   editfunc    EFblurBG;
}


//  called from main blur function, no separate menu

void m_blur_background(GtkWidget *, const char *)
{
   using namespace blur_BG_names;

   int    blur_BG_dialog_event(zdialog* zd, const char *event);
   void * blur_BG_thread(void *);

   EFblurBG.menufunc = m_blur_background;
   EFblurBG.funcname = "blur_background";                                        //  function name
   EFblurBG.Farea = 2;                                                           //  select area usable (required)
   EFblurBG.threadfunc = blur_BG_thread;                                         //  thread function
      
   if (! edit_setup(EFblurBG)) return;                                           //  setup edit

   minbrad = 10;                                                                 //  defaults
   maxbrad = 20;
   conbrad = 10;
   conrad = 1;
   incrad = 0;
   Fcancel = 0;

   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;
   
/***
       ____________________________________
      |          Blur Background           |
      |                                    |
      |  [x] constant blur [ 20 ]          |
      |                                    |
      |  [x] increase blur with distance   |
      |    min. blur radius [ 20 ]         |
      |    max. blur radius [ 90 ]         |
      |                                    |
      |            [Apply] [Done] [Cancel] |
      |____________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Blur Background"),Mwin,Bapply,Bdone,Bcancel,null);
   CEF->zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbcon","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","conrad","hbcon",E2X("constant blur"),"space=3");
   zdialog_add_widget(zd,"zspin","conbrad","hbcon","1|100|1|10","space=8");
   zdialog_add_widget(zd,"hbox","hbinc","dialog");
   zdialog_add_widget(zd,"check","incrad","hbinc",E2X("increase blur with distance"),"space=3");
   zdialog_add_widget(zd,"hbox","hbmin","dialog");
   zdialog_add_widget(zd,"label","labmin","hbmin",E2X("min. blur radius"),"space=8");
   zdialog_add_widget(zd,"zspin","minbrad","hbmin","0|100|1|10","space=3");
   zdialog_add_widget(zd,"hbox","hbmax","dialog");
   zdialog_add_widget(zd,"label","labmax","hbmax",E2X("max. blur radius"),"space=8");
   zdialog_add_widget(zd,"zspin","maxbrad","hbmax","1|100|1|20","space=3");
   
   zdialog_stuff(zd,"conrad",conrad);
   zdialog_stuff(zd,"incrad",incrad);

   zdialog_resize(zd,300,0);
   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_run(zd,blur_BG_dialog_event,"save");                                  //  run dialog - parallel
   
   if (sa_stat != 3) m_select(0,0);                                              //  start select area dialog
   return;
}


//  blur_BG dialog event and completion function

int blur_BG_dialog_event(zdialog *zd, const char *event)                         //  blur_BG dialog event function
{
   using namespace blur_BG_names;
   
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [apply]
      {
         zd->zstat = 0;                                                          //  keep dialog active

         if (sa_stat != 3) {
            zmessageACK(Mwin,E2X("no active Select Area"));
            m_select(0,0);
            return 1;
         }
         
         if (incrad && ! sa_calced)                                              //  if increasing blur radius,
            sa_edgecalc();                                                       //    calc. area edge distances

         sa_show(0,0);
         edit_reset();
         signal_thread();
         return 1;
      }
      
      else if (zd->zstat == 2) {
         edit_done(0);                                                           //  [done]
         if (zd_sela) zdialog_send_event(zd_sela,"done");                        //  kill select area dialog
      }

      else {
         Fcancel = 1;                                                            //  kill threads
         edit_cancel(0);                                                         //  [cancel] or [x], discard edit
         if (zd_sela) zdialog_send_event(zd_sela,"done");
      }

      return 1;
   }
   
   if (zstrstr("conrad incrad",event)) {
      zdialog_stuff(zd,"conrad",0);
      zdialog_stuff(zd,"incrad",0);
      zdialog_stuff(zd,event,1);
   }
   
   zdialog_fetch(zd,"conrad",conrad);
   zdialog_fetch(zd,"incrad",incrad);
   zdialog_fetch(zd,"conbrad",conbrad);
   zdialog_fetch(zd,"minbrad",minbrad);
   zdialog_fetch(zd,"maxbrad",maxbrad);
   
   return 1;
}


//  thread function - multiple working threads to update image

void * blur_BG_thread(void *)
{
   using namespace blur_BG_names;

   void  * blur_BG_wthread(void *arg);                                           //  worker thread
   
   int      ii, dist;

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      if (incrad && sa_calced) {                                                 //  if increasing blur radius,
         maxdist = 0;                                                            //    get max. area edge distance
         for (ii = 0; ii < E3ww * E3hh; ii++) {
            dist = sa_pixmap[ii];
            if (dist > maxdist) maxdist = dist;
         }
      }

      Fbusy_goal = sa_Npixel;
      Fbusy_done = 0;

      do_wthreads(blur_BG_wthread,NWT);                                          //  worker threads
      
      Fbusy_goal = Fbusy_done = 0;

      CEF->Fmods++;                                                              //  image modified
      CEF->Fsaved = 0;                                                           //  not saved

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

   return 0;                                                                     //  not executed, stop warning
}


void * blur_BG_wthread(void *arg)                                                //  worker thread function
{
   using namespace blur_BG_names;

   int         index = *((int *) (arg));
   int         ii, rad = 0, dist, npix;
   int         px, py, qx, qy;
   float       *pix1, *pix3;
   float       red, green, blue, F;
   
   for (py = index; py < E3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < E3ww; px++)
   {
      if (Fcancel) break;                                                        //  cancel edit

      ii = py * E3ww + px;
      dist = sa_pixmap[ii];                                                      //  area edge distance
      if (! dist) continue;                                                      //  pixel outside the area
      
      if (conrad) rad = conbrad;                                                 //  use constant blur radius

      if (incrad) {                                                              //  use increasing blur radius
         if (! sa_calced) pthread_exit(0);                                       //    depending on edge distance
         rad = minbrad + (maxbrad - minbrad) * dist / maxdist;
      }

      npix = 0;
      red = green = blue = 0;      

      for (qy = py-rad; qy <= py+rad; qy++)                                      //  average surrounding pixels
      for (qx = px-rad; qx <= px+rad; qx++)
      {
         if (qy < 0 || qy > E3hh-1) continue;
         if (qx < 0 || qx > E3ww-1) continue;
         ii = qy * E3ww + qx;
         if (! sa_pixmap[ii]) continue;
         pix1 = PXMpix(E1pxm,qx,qy);
         red += pix1[0];
         green += pix1[1];
         blue += pix1[2];
         npix++;
      }
      
      F = 0.999 / npix;
      red = F * red;                                                             //  blurred pixel RGB
      green = F * green;
      blue = F * blue;
      pix3 = PXMpix(E3pxm,px,py);
      pix3[0] = red;
      pix3[1] = green;
      pix3[2] = blue;

      Fbusy_done++;                                                              //  count pixels done
   }
   
   pthread_exit(0);                                                              //  exit thread
}


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

//  image noise reduction

namespace denoise_names
{
   enum     denoise_method { voodoo, chroma, anneal, flatten, median, tophat, wavelets }
            denoise_method;

   int      noise_histogram[3][256];
   int      denoise_radius;
   float    denoise_thresh, wavelets_thresh;
   float    denoise_darkareas;

   zdialog  *zd_denoise_measure;
   cchar    *mformat = "  mean RGB:  %5.0f %5.0f %5.0f ";
   cchar    *nformat = " mean noise: %5.2f %5.2f %5.2f ";
   
   int      E3ww, E3hh;                                                          //  image dimensions
   int8     *Fpix, *Gpix;

   editfunc    EFdenoise;
   GtkWidget   *denoise_measure_drawwin;
}


//  menu function

void m_denoise(GtkWidget *, cchar *menu)
{
   using namespace denoise_names;

   void   denoise_characterize();
   int    denoise_dialog_event(zdialog *zd, cchar *event);
   void * denoise_thread(void *);
   
   cchar  *tip = E2X("Apply repeatedly while watching the image.");

   F1_help_topic = "denoise";

   EFdenoise.menuname = menu;
   EFdenoise.menufunc = m_denoise;
   EFdenoise.funcname = "denoise";
   EFdenoise.Farea = 2;                                                          //  select area usable
   EFdenoise.threadfunc = denoise_thread;                                        //  thread function
   EFdenoise.Frestart = 1;                                                       //  allow restart
   EFdenoise.FusePL = 1;                                                         //  use with paint/lever edits OK
   EFdenoise.Fscript = 1;                                                        //  scripting supported 
   if (! edit_setup(EFdenoise)) return;                                          //  setup edit

   E3ww = E3pxm->ww;                                                             //  image dimensions
   E3hh = E3pxm->hh;

/***
          _____________________________________________
         |           Noise Reduction                   |
         |  Apply repeatedly while watching the image. |
         | - - - - - - - - - - - - - - - - - - - - - - |          sep0
         |  Method                                     |          hbm1
         |    (o) Voodoo   (o) Chroma   (o) Anneal     |          hbm2  vbm1  vbm2  vbm3
         |    (o) Flatten  (o) Median   (o) Top Hat    |          
         |  Radius [___]    Threshold [___]            |          hbrt
         | - - - - - - - - - - - - - - - - - - - - - - |          sep1
         |    (o) Wavelets Threshold [___]             |          hbwav
         | - - - - - - - - - - - - - - - - - - - - - - |          sep2
         | dark areas =========[]=========== all areas |          hbar
         |                                             |
         |   [Measure] [Apply] [Reset] [Done] [Cancel] |
         |_____________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Noise Reduction"),Mwin,Bmeasure,Bapply,Breset,Bdone,Bcancel,null);
   EFdenoise.zd = zd;
   
   zdialog_add_widget(zd,"label","labtip","dialog",tip,"space=3");
   
   zdialog_add_widget(zd,"hsep","sep0","dialog",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbm1","dialog",0);
   zdialog_add_widget(zd,"label","labm","hbm1","Method","space=3");

   zdialog_add_widget(zd,"hbox","hbm2","dialog",0);
   zdialog_add_widget(zd,"label","space","hbm2",0,"space=8"); 
   zdialog_add_widget(zd,"vbox","vbm1","hbm2",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbm2","hbm2",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbm3","hbm2",0,"space=3");
   zdialog_add_widget(zd,"check","voodoo","vbm1","Voodoo");
   zdialog_add_widget(zd,"check","chroma","vbm2","Chroma");
   zdialog_add_widget(zd,"check","anneal","vbm3","Anneal");
   zdialog_add_widget(zd,"check","flatten","vbm1","Flatten");
   zdialog_add_widget(zd,"check","median","vbm2","Median");
   zdialog_add_widget(zd,"check","tophat","vbm3","Top Hat");
   
   zdialog_add_widget(zd,"hbox","hbrt","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labrad","hbrt",Bradius,"space=3");
   zdialog_add_widget(zd,"zspin","denoise_radius","hbrt","1|9|1|3","size=4");
   zdialog_add_widget(zd,"label","space","hbrt",0,"space=8");
   zdialog_add_widget(zd,"label","labthresh","hbrt",Bthresh,"space=3");
   zdialog_add_widget(zd,"zspin","denoise_thresh","hbrt","1|50|1|10","size=4");

   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbwav","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","space","hbwav",0,"space=8"); 
   zdialog_add_widget(zd,"check","wavelets","hbwav","Wavelets");
   zdialog_add_widget(zd,"label","labthresh","hbwav",Bthresh);
   zdialog_add_widget(zd,"zspin","wavelets_thresh","hbwav","0.1|8.0|0.1|1","size=4|space=12");

   zdialog_add_widget(zd,"hsep","sep2","dialog",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbar","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labdark","hbar",E2X("dark areas"),"space=8");
   zdialog_add_widget(zd,"hscale","darkareas","hbar","0|256|1|256","expand");
   zdialog_add_widget(zd,"label","laball","hbar",E2X("all areas"),"space=8");

   denoise_characterize();                                                       //  characterize image noise           20.0
   zdialog_stuff(zd,"denoise_thresh",denoise_thresh);                            //    = default threshold

   wavelets_thresh = 1.0;                                                        //  default wavelets threshold
   
   zdialog_stuff(zd,"voodoo",1);                                                 //  initial selected method

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


//  characterize noise levels

void denoise_characterize()                                                      //  20.0
{
   using namespace denoise_names;

   void * denoise_characterize_wthread(void *arg);

   int      ii, rgb, Npix, Tpix;
   double   val, sum, sum2, mean, sigma, varnc, thresh, limit;

   for (rgb = 0; rgb < 3; rgb++)                                                 //  clear histogram
   for (ii = 0; ii < 256; ii++)
      noise_histogram[rgb][ii] = 0;

   do_wthreads(denoise_characterize_wthread,NWT);                                //  make histogram of pixel-pixel diffs

   thresh = 0;
   limit = 100;

   for (rgb = 0; rgb < 3; rgb++)                                                 //  get mean and sigma
   {
      sum = sum2 = Tpix = 0;
      
      for (ii = 0; ii < limit; ii++) {                                           //  omit diffs > limit
         Npix = noise_histogram[rgb][ii];
         Tpix += Npix;
         val = ii * Npix;
         sum += val;
         sum2 += val * val;
      }
      
      mean = sum / limit;
      varnc = (sum2 - 2.0 * mean * mean) / limit + mean * mean;
      sigma = sqrtf(varnc);
      
      mean = mean / Tpix * limit;                                                //  mean and sigma
      sigma = sigma / Tpix * limit;
      if (sigma > thresh) thresh = sigma;
   }
   
   sigma = thresh;                                                               //  greatest RGB sigma
   
   thresh = 0;
   limit = 3 * sigma;
   
   for (rgb = 0; rgb < 3; rgb++)                                                 //  make another histogram
   {                                                                             //    ignoring values > 3 * sigma
      sum = sum2 = Tpix = 0;
      
      for (ii = 0; ii < limit; ii++) {
         Npix = noise_histogram[rgb][ii];
         Tpix += Npix;
         val = ii * Npix;
         sum += val;
         sum2 += val * val;
      }
      
      mean = sum / limit;
      varnc = (sum2 - 2.0 * mean * mean) / limit + mean * mean;
      sigma = sqrtf(varnc);
      
      mean = mean / Tpix * limit;
      sigma = sigma / Tpix * limit;
      if (sigma > thresh) thresh = sigma;
   }
   
   denoise_thresh = thresh;                                                      //  greatest RGB sigma
   return;
}


void * denoise_characterize_wthread(void *arg)
{
   using namespace denoise_names;

   int      index = *((int *) arg);
   int      ii, px, py, diff, rgb;
   float    *pix1, *pix2;
   
   for (py = index; py < E3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < E3ww-1; px++)                                               //  omit last column
   {
      if (sa_stat == 3) {                                                        //  select area active                 20.0
         ii = py * E3ww + px;
         if (sa_pixmap[ii] == 0) continue;                                       //  pixel outside area, ignore
      }

      pix1 = PXMpix(E1pxm,px,py);
      pix2 = PXMpix(E1pxm,px+1,py);
      
      for (rgb = 0; rgb < 3; rgb++)
      {
         diff = pix1[rgb] - pix2[rgb];                                           //  pixel-pixel RGB difference
         diff = abs(diff);
         noise_histogram[rgb][diff] += 1;
      }
   }
   
   pthread_exit(0);
   return 0;
}


//  dialog event and completion callback function

int denoise_dialog_event(zdialog * zd, cchar *event)                             //  reworked for script files
{
   using namespace denoise_names;

   void   denoise_measure();

   int    ii;
   
   wait_thread_idle();
   
   if (strmatch(event,"apply")) zd->zstat = 2;                                   //  from script file
   if (strmatch(event,"done")) zd->zstat = 4;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 5;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  [measure] 
         zd->zstat = 0;                                                          //  keep dialog active
         denoise_measure();
         return 1;
      }
      
      if (zd->zstat == 2) {                                                      //  [apply]
         zd->zstat = 0;                                                          //  keep dialog active
         signal_thread();
         return 1;
      }

      if (zd->zstat == 3) {                                                      //  [reset]
         edit_undo();                                                            //  undo edits
         zd->zstat = 0;                                                          //  keep dialog active
         return 1;
      }

      if (zd->zstat == 4) edit_done(0);                                          //  [done]  commit edit
      else edit_cancel(0);                                                       //  [cancel] or [x]  discard edit

      if (zd_denoise_measure) {                                                  //  kill measure dialog 
         freeMouse();
         zdialog_free(zd_denoise_measure);
         zd_denoise_measure = 0;
      }

      return 1;
   }
   
   if (strmatch(event,"blendwidth")) signal_thread();

   zdialog_fetch(zd,"denoise_radius",denoise_radius);
   zdialog_fetch(zd,"denoise_thresh",denoise_thresh);
   zdialog_fetch(zd,"wavelets_thresh",wavelets_thresh);
   zdialog_fetch(zd,"darkareas",denoise_darkareas);
   
   if (zstrstr("voodoo chroma anneal flatten median tophat wavelets",event)) {   //  capture choice
      zdialog_stuff(zd,"voodoo",0);
      zdialog_stuff(zd,"chroma",0);
      zdialog_stuff(zd,"anneal",0);
      zdialog_stuff(zd,"flatten",0);
      zdialog_stuff(zd,"median",0);
      zdialog_stuff(zd,"tophat",0);
      zdialog_stuff(zd,"wavelets",0);
      zdialog_stuff(zd,event,1);
   }
   
   zdialog_fetch(zd,"voodoo",ii);                                                //  20.0
   if (ii) denoise_method = voodoo;

   zdialog_fetch(zd,"chroma",ii);                                                //  20.0
   if (ii) denoise_method = chroma;

   zdialog_fetch(zd,"anneal",ii);                                                //  20.0
   if (ii) denoise_method = anneal;

   zdialog_fetch(zd,"flatten",ii);
   if (ii) denoise_method = flatten;

   zdialog_fetch(zd,"median",ii);
   if (ii) denoise_method = median;
   
   zdialog_fetch(zd,"tophat",ii);
   if (ii) denoise_method = tophat;
   
   zdialog_fetch(zd,"wavelets",ii);
   if (ii) denoise_method = wavelets;

   return 1;
}


//  image noise reduction thread

void * denoise_thread(void *)
{
   using namespace denoise_names;

   void * denoise_chroma_wthread1(void *arg);
   void * denoise_chroma_wthread2(void *arg);
   void * denoise_anneal_wthread(void *arg);
   void * denoise_flatten_wthread(void *arg);
   void * denoise_median_wthread(void *arg);
   void * denoise_tophat_wthread(void *arg);
   void * denoise_wavelets_wthread(void *arg);

   int      ii, px, py, dist = 0;
   float    *pix1, *pix9;
   float    save_thresh;
   int      nc = E3pxm->nc, pcc = nc * sizeof(float);

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

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

      E9pxm = PXM_copy(E3pxm);                                                   //  E3 is source, E9 is modified
      
      if (denoise_method == wavelets)                                            //  wavelets method
         do_wthreads(denoise_wavelets_wthread,3);                                //  worker threads, 1 per RGB color

      else                                                                       //  other method
      {
         if (sa_stat == 3) Fbusy_goal = sa_Npixel;
         else  Fbusy_goal = E3ww * E3hh;
         Fbusy_done = 0;

         if (denoise_method == voodoo)                                           //  20.0
         {
            save_thresh = denoise_thresh;
            Fbusy_goal *= 4;
            denoise_thresh = 255;                                                //  salt and pepper elimination
            denoise_radius = 1;
            do_wthreads(denoise_median_wthread,NWT);
 
            denoise_thresh = save_thresh;

            for (ii = 0; ii < 3; ii++)
            {
               PXM_free(E3pxm);                                                  //  accumulate changes
               E3pxm = E9pxm;
               E9pxm = PXM_copy(E3pxm);
               denoise_radius = 2 + 2 * ii;                                      //  2  4  6
               if (ii > 0) denoise_thresh *= 0.8;                                //  1.0  0.8  0.64
               do_wthreads(denoise_anneal_wthread,NWT);                          //  general noise reduction
            }

            denoise_thresh = save_thresh;
         }
     
         if (denoise_method == chroma) {                                         //  20.0
            E8pxm = PXM_make(E3ww,E3hh,3);                                       //  allocate HSL image
            do_wthreads(denoise_chroma_wthread1,NWT);
            do_wthreads(denoise_chroma_wthread2,NWT);
            PXM_free(E8pxm);
         }

         if (denoise_method == anneal)                                           //  20.0
            do_wthreads(denoise_anneal_wthread,NWT);

         if (denoise_method == flatten)
            do_wthreads(denoise_flatten_wthread,NWT);

         if (denoise_method == median)
            do_wthreads(denoise_median_wthread,NWT);

         if (denoise_method == tophat)
            do_wthreads(denoise_tophat_wthread,NWT);

         Fbusy_goal = 0;
      }

      if (denoise_darkareas < 256)                                               //  if brightness threshold set,      19.5
      {                                                                          //    revert brighter areas
         for (py = 0; py < E3hh; py++)
         for (px = 0; px < E3ww; px++)
         {
            if (sa_stat == 3) {                                                  //  select area active
               ii = py * E3ww + px;
               dist = sa_pixmap[ii];                                             //  distance from edge
               if (! dist) continue;                                             //  outside pixel
            }

            pix1 = PXMpix(E1pxm,px,py);                                          //  source pixel                       19.0
            pix9 = PXMpix(E9pxm,px,py);                                          //  target pixel

            if (pix1[0] + pix1[1] + pix1[2] > 3 * denoise_darkareas)             //  revert brighter pixels
               memcpy(pix9,pix1,pcc);
         }
      }

      PXM_free(E3pxm);                                                           //  image9 >> image3
      E3pxm = E9pxm;
      E9pxm = 0;

      CEF->Fmods++;
      CEF->Fsaved = 0;

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

   return 0;                                                                     //  not executed, stop g++ warning
}


//  Chroma:
//  Exchange color data between neighbor pixels that match closely enough.

void RGB_YCbCr(float R, float G, float B, float &Y, float &Cb, float &Cr)
{
   Y  =  0.299 * R + 0.587 * G + 0.114 * B;
   Cb =  128 - 0.168736 * R - 0.331264 * G + 0.5 * B;
   Cr =  128 + 0.5 * R - 0.418688 * G - 0.081312 * B;
   return;
}

void YCbCr_RGB(float Y, float Cb, float Cr, float &R, float &G, float &B)
{
   R = Y + 1.402 * (Cr - 128);
   G = Y - 0.344136 * (Cb - 128) - 0.714136 * (Cr - 128);
   B = Y + 1.772 * (Cb - 128);
   return;
}


void * denoise_chroma_wthread1(void *arg)                                        //  20.0
{
   using namespace denoise_names;

   int         index = *((int *) arg);
   int         px, py;
   float       *pix3, *pix8;

   for (py = index; py < E3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < E3ww; px++)
   {
      pix3 = PXMpix(E3pxm,px,py);                                                //  E3 RGB image >> E8 YCbCr image
      pix8 = PXMpix(E8pxm,px,py);
      RGB_YCbCr(pix3[0],pix3[1],pix3[2],pix8[0],pix8[1],pix8[2]);
   }

   pthread_exit(0);
}


void * denoise_chroma_wthread2(void *arg)                                        //  20.0
{
   using namespace denoise_names;

   int         index = *((int *) arg);
   int         rad, thresh;
   int         ii, dist = 0;
   int         px, py, dx, dy, rgb;
   float       *pix1, *pix9;
   float       *pix8, *pix8N, pixM[3];
   float       Frad, f1, f2;
   float       match, reqmatch;
   
   rad = denoise_radius;
   thresh = denoise_thresh;
   reqmatch = 1.0 - 0.01 * thresh;                                               //  20 >> 0.8
   Frad = rad + rad + 1;
   Frad = 1.0 / (Frad * Frad);

   for (py = index+rad; py < E3hh-rad; py += NWT)                                //  loop all image pixels
   for (px = rad; px < E3ww-rad; px++)                                           //  skip outermost rows and cols
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pix8 = PXMpix(E8pxm,px,py);                                                //  source YCbCr pixel

      memset(pixM,0,3*sizeof(float));                                            //  clear totals

      for (dy = -rad; dy <= +rad; dy++)                                          //  loop neighbor pixels
      for (dx = -rad; dx <= +rad; dx++)
      {
         pix8N = pix8 + (dy * E3ww + dx) * 3;                                    //  neighbor pixel
         
         match = PIXMATCH(pix8,pix8N);                                           //  match level, 0..1 = perfect match
         if (match > reqmatch) f1 = 0.5;                                         //  color exchange factor
         else f1 = 0;
         f2 = 1.0 - f1;

         for (rgb = 0; rgb < 3; rgb++)
            pixM[rgb] += f2 * pix8[rgb] + f1 * pix8N[rgb];                       //  accumulate exchange colors
      }

      for (rgb = 0; rgb < 3; rgb++)                                              //  normalize to 0-255
         pixM[rgb] = pixM[rgb] * Frad;      
      
      match = PIXMATCH(pix8,pixM);                                               //  final old:new comparison
      if (match < reqmatch) continue;                                            //  reject larger changes
      
      pix9 = PXMpix(E9pxm,px,py);                                                //  target RGB pixel
      YCbCr_RGB(pixM[0],pixM[1],pixM[2],pix9[0],pix9[1],pix9[2]);                //  YCbCr pix8 >> RGB pix9

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //  blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         pix1 = PXMpix(E1pxm,px,py);                                             //  source image pixel
         pix9[0] = f1 * pix9[0] + f2 * pix1[0];
         pix9[1] = f1 * pix9[1] + f2 * pix1[1];
         pix9[2] = f1 * pix9[2] + f2 * pix1[2];
      }

      Fbusy_done++;                                                              //  track progress
   }

   pthread_exit(0);
}


//  Anneal:
//  Exchange RGB data between neighbor pixels that match closely enough.

void * denoise_anneal_wthread(void *arg)                                         //  20.0
{
   using namespace denoise_names;

   int         index = *((int *) arg);
   int         rad, thresh;
   int         ii, rgb, dist = 0;
   int         px, py, dx, dy;
   int         nc = E3pxm->nc;
   float       *pix1, *pix3, *pix9, *pix3N;
   float       f1, f2, Frad, pixM[3];
   float       match, reqmatch;

   rad = denoise_radius;
   thresh = denoise_thresh;
   reqmatch = 1.0 - 0.01 * thresh;                                               //  20 >> 0.8
   Frad = rad + rad + 1;
   Frad = 1.0 / (Frad * Frad);
   
   for (py = index+rad; py < E3hh-rad; py += NWT)                                //  loop all image pixels
   for (px = rad; px < E3ww-rad; px++)                                           //  skip outermost rows and cols
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      for (rgb = 0; rgb < 3; rgb++)
         pixM[rgb] = 0;

      pix1 = PXMpix(E1pxm,px,py);
      pix3 = PXMpix(E3pxm,px,py);                                                //  source image pixel
      pix9 = PXMpix(E9pxm,px,py);                                                //  target image pixel

      for (dy = -rad; dy <= +rad; dy++)                                          //  loop neighbor pixels
      for (dx = -rad; dx <= +rad; dx++)
      {
         pix3N = pix3 + (dy * E3ww + dx) * nc;                                   //  neighbor pixel
         match = PIXMATCH(pix3,pix3N);                                           //  match level, 0..1 = perfect match
         if (match > reqmatch) f1 = 0.5;                                         //  RGB exchange factors
         else f1 = 0;
         f2 = 1.0 - f1;

         for (rgb = 0; rgb < 3; rgb++)                                           //  loop all RGB colors
            pixM[rgb] += f2 * pix3[rgb] + f1 * pix3N[rgb];                       //  accumulate exchange colors
      }
      
      for (rgb = 0; rgb < 3; rgb++)                                              //  normalize to 0-255
         pixM[rgb] = Frad * pixM[rgb];
      
      match = PIXMATCH(pix1,pixM);                                               //  final old:new comparison
      if (match < reqmatch) continue;                                            //  reject larger changes
      
      for (rgb = 0; rgb < 3; rgb++)
         pix9[rgb] = pixM[rgb];

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //  blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         pix9[0] = f1 * pix9[0] + f2 * pix1[0];
         pix9[1] = f1 * pix9[1] + f2 * pix1[1];
         pix9[2] = f1 * pix9[2] + f2 * pix1[2];
      }

      Fbusy_done++;                                                              //  track progress
   }

   pthread_exit(0);
}


//  Flatten: 
//  Flatten outlyer pixels within neighborhood group.
//  An outlier pixel has an RGB value outside mean +- sigma.
//  Process groups increasing from radius = 1 to denoise_radius.

void * denoise_flatten_wthread(void *arg)
{
   using namespace denoise_names;

   int         index = *((int *) arg);
   int         ii, rgb, dist = 0;
   int         px, py, dx, dy, rad1, rad2, thresh;
   int         nc = E3pxm->nc;
   float       *pix1, *pix3, *pix9, *pixN;
   float       nn, val, sum, sum2, mean, variance, sigma, thresh2;
   float       f1, f2;

   rad1 = denoise_radius;
   thresh = denoise_thresh;

   for (py = index+rad1; py < E3hh-rad1; py += NWT)                              //  loop all image pixels
   for (px = rad1; px < E3ww-rad1; px++)                                         //  skip outermost rows and cols
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pix3 = PXMpix(E3pxm,px,py);                                                //  source image pixel
      pix9 = PXMpix(E9pxm,px,py);                                                //  target image pixel

      for (rad2 = 1; rad2 <= rad1; rad2++)                                       //  radius from 1 to max               20.0
      {
         nn = (rad2 * 2 + 1);                                                    //  pixel group NxN size
         nn = nn * nn;                                                           //  pixels in group

         for (rgb = 0; rgb < 3; rgb++)                                           //  loop RGB color
         {
            sum = sum2 = 0;

            for (dy = -rad2; dy <= rad2; dy++)                                   //  loop surrounding pixels
            for (dx = -rad2; dx <= rad2; dx++)
            {
               pixN = pix3 + (dy * E3ww + dx) * nc;
               val = pixN[rgb];
               sum += val;
               sum2 += val * val;
            }

            mean = sum / nn;                                                     //  compute mean and sigma
            variance = (sum2 - 2.0 * mean * sum) / nn + mean * mean;
            sigma = sqrtf(variance);
            thresh2 = 0.5 * (thresh + sigma);

            if (pix3[rgb] > mean + sigma && pix3[rgb] - mean < thresh2)          //  if | pixel - mean | > sigma        20.0
               pix9[rgb] = mean;                                                 //    flatten pixel
            else if (pix3[rgb] < mean - sigma && mean - pix3[rgb] < thresh2)
               pix9[rgb] = mean;
         }
      }

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //  blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         pix1 = PXMpix(E1pxm,px,py);                                             //  source image pixel
         pix9[0] = f1 * pix9[0] + f2 * pix1[0];
         pix9[1] = f1 * pix9[1] + f2 * pix1[1];
         pix9[2] = f1 * pix9[2] + f2 * pix1[2];
      }

      Fbusy_done++;                                                              //  track progress
   }

   pthread_exit(0);
}


//  Median:
//  Use median RGB brightness for pixels within radius

void * denoise_median_wthread(void *arg)
{
   using namespace denoise_names;

   int         index = *((int *) arg);
   int         ii, rgb, dist = 0;
   int         px, py, dx, dy, rad, Frad, thresh;
   int         nc = E3pxm->nc;
   float       *pix1, *pix3, *pix9, *pixN;
   float       f1, f2, median;
   int16       rgbdist[256];
   int         rgbsum;

   rad = denoise_radius;
   thresh = denoise_thresh;

   Frad = 2 * rad + 1;
   Frad = Frad * Frad;

   for (py = index+rad; py < E3hh-rad; py += NWT)                                //  loop all image pixels
   for (px = rad; px < E3ww-rad; px++)                                           //  skip outermost rows and cols
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pix3 = PXMpix(E3pxm,px,py);                                                //  source image pixel
      pix9 = PXMpix(E9pxm,px,py);                                                //  target image pixel

      for (rgb = 0; rgb < 3; rgb++)                                              //  loop all RGB colors
      {
         memset(rgbdist,0,256*sizeof(int16));                                    //  clear rgb distribution
         rgbsum = 0;

         for (dy = -rad; dy <= rad; dy++)                                        //  loop surrounding pixels
         for (dx = -rad; dx <= rad; dx++)                                        //  get RGB values
         {
            pixN = pix3 + (dy * E3ww + dx) * nc;                                 //  make distribution of RGB values    20.0
            ii = pixN[rgb];
            rgbdist[ii]++;
         }
         
         for (ii = 0; ii < 256; ii++)                                            //  sum distribution from 0 to 255     20.0
         {
            rgbsum += rgbdist[ii];                                               //  break when half of RGB values
            if (rgbsum > Frad / 2) break;                                        //    have been counted
         }
         
         median = ii;                                                            //  >> median RGB value

         if (pix3[rgb] > median && pix3[rgb] - median < thresh)                  //  if | rgb - median | < threshold    20.0
            pix9[rgb] = median;                                                  //    moderate rgb

         else if (pix3[rgb] < median && median - pix3[rgb] < thresh)
            pix9[rgb] = median;
      }

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //  blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         pix1 = PXMpix(E1pxm,px,py);                                             //  source image pixel
         pix9[0] = f1 * pix9[0] + f2 * pix1[0];
         pix9[1] = f1 * pix9[1] + f2 * pix1[1];
         pix9[2] = f1 * pix9[2] + f2 * pix1[2];
      }

      Fbusy_done++;                                                              //  track progress
   }

   pthread_exit(0);
}


//  Top Hat:
//  Execute with increasing radius from 1 to limit.
//  Detect outlier by comparing with pixels along outer radius only.

void * denoise_tophat_wthread(void *arg)
{
   using namespace denoise_names;

   int         index = *((int *) arg);
   int         ii, dist = 0;
   int         px, py, dx, dy, rad1, rad2;
   int         nc = E3pxm->nc;
   float       *pix1, *pix3, *pix9, *pixN;
   float       minR, minG, minB, maxR, maxG, maxB;
   float       f1, f2;

   rad1 = denoise_radius;

   for (py = index+rad1; py < E3hh-rad1; py += NWT)                              //  loop all image pixels
   for (px = rad1; px < E3ww-rad1; px++)                                         //  skip outermost rows and cols
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pix3 = PXMpix(E3pxm,px,py);                                                //  source image pixel
      pix9 = PXMpix(E9pxm,px,py);                                                //  target image pixel

      for (rad2 = 1; rad2 <= rad1; rad2++)                                       //  loop rad2 from 1 to max.
      for (int loops = 0; loops < 2; loops++)
      {
         minR = minG = minB = 255;
         maxR = maxG = maxB = 0;

         for (dy = -rad2; dy <= rad2; dy++)                                      //  loop all pixels within rad2
         for (dx = -rad2; dx <= rad2; dx++)
         {
            if (dx > -rad2 && dx < rad2) continue;                               //  skip inner pixels
            if (dy > -rad2 && dy < rad2) continue;

            pixN = pix3 + (dy * E3ww + dx) * nc;
            if (pixN[0] < minR) minR = pixN[0];                                  //  find min and max per color
            if (pixN[0] > maxR) maxR = pixN[0];                                  //    among outermost pixels
            if (pixN[1] < minG) minG = pixN[1];
            if (pixN[1] > maxG) maxG = pixN[1];
            if (pixN[2] < minB) minB = pixN[2];
            if (pixN[2] > maxB) maxB = pixN[2];
         }

         if (pix3[0] < minR && pix9[0] < 254) pix9[0] += 2;                      //  if central pixel is outlier,       20.0
         if (pix3[0] > maxR && pix9[0] > 1)   pix9[0] -= 2;                      //    moderate its values
         if (pix3[1] < minG && pix9[1] < 254) pix9[1] += 2;
         if (pix3[1] > maxG && pix9[1] > 1)   pix9[1] -= 2;
         if (pix3[2] < minB && pix9[2] < 254) pix9[2] += 2;
         if (pix3[2] > maxB && pix9[2] > 1)   pix9[2] -= 2;
      }

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //  blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         pix1 = PXMpix(E1pxm,px,py);                                             //  source image pixel
         pix9[0] = f1 * pix9[0] + f2 * pix1[0];
         pix9[1] = f1 * pix9[1] + f2 * pix1[1];
         pix9[2] = f1 * pix9[2] + f2 * pix1[2];
      }

      Fbusy_done++;                                                              //  track progress
   }

   pthread_exit(0);
}


//  Wavelets:
//  worker thread for wavelets method
//  do wavelets denoise for one color in each of 3 threads

void * denoise_wavelets_wthread(void *arg)
{
   using namespace denoise_names;

   void denoise_wavelets(float *fimg[3], uint ww2, uint hh2, float);

   int      rgb = *((int *) arg);                                                //  rgb color 0/1/2
   int      ii, jj;
   float    *fimg[3];
   float    f256 = 1.0 / 256.0;
   int      nc = E3pxm->nc;

   if (sa_stat == 3) goto denoise_area;                                          //  select area is active

   fimg[0] = (float *) zmalloc(E3ww * E3hh * sizeof(float));
   fimg[1] = (float *) zmalloc(E3ww * E3hh * sizeof(float));
   fimg[2] = (float *) zmalloc(E3ww * E3hh * sizeof(float));

   for (ii = 0; ii < E3ww * E3hh; ii++)                                          //  extract one noisy color from E3
      fimg[0][ii] = E3pxm->pixels[nc*ii+rgb] * f256;

   denoise_wavelets(fimg,E3ww,E3hh,wavelets_thresh);

   for (ii = 0; ii < E3ww * E3hh; ii++)                                          //  save one denoised color to E9
      E9pxm->pixels[nc*ii+rgb] = 256.0 * fimg[0][ii];

   zfree(fimg[0]);
   zfree(fimg[1]);
   zfree(fimg[2]);

   pthread_exit(0);

denoise_area:

   int      px, py, pxl, pxh, pyl, pyh, ww2, hh2, dist;
   float    f1, f2;

   pxl = sa_minx - 16;
   if (pxl < 0) pxl = 0;
   pxh = sa_maxx + 16;
   if (pxh > E3ww) pxh = E3ww;

   pyl = sa_miny - 16;
   if (pyl < 0) pyl = 0;
   pyh = sa_maxy + 16;
   if (pyh > E3hh) pyh = E3hh;

   ww2 = pxh - pxl;
   hh2 = pyh - pyl;

   fimg[0] = (float *) zmalloc(ww2 * hh2 * sizeof(float));
   fimg[1] = (float *) zmalloc(ww2 * hh2 * sizeof(float));
   fimg[2] = (float *) zmalloc(ww2 * hh2 * sizeof(float));

   for (py = 0; py < hh2; py++)
   for (px = 0; px < ww2; px++)
   {
      ii = py * ww2 + px;
      jj = (py + pyl) * E3ww + (px + pxl);
      fimg[0][ii] = E3pxm->pixels[nc*jj+rgb] * f256;
   }

   denoise_wavelets(fimg,ww2,hh2,wavelets_thresh);

   for (py = 0; py < hh2; py++)
   for (px = 0; px < ww2; px++)
   {
      ii = py * ww2 + px;
      jj = (py + pyl) * E3ww + (px + pxl);

      dist = sa_pixmap[jj];
      if (! dist) continue;

      if (dist < sa_blendwidth) {
         f1 = sa_blendfunc(dist);
         f2 = 1.0 - f1;
         E9pxm->pixels[nc*jj+rgb] = f1 * 256.0 * fimg[0][ii] + f2 * E3pxm->pixels[nc*jj+rgb];
      }
      else E9pxm->pixels[nc*jj+rgb] = 256.0 * fimg[0][ii];
   }

   zfree(fimg[0]);
   zfree(fimg[1]);
   zfree(fimg[2]);

   pthread_exit(0);
}


//  wavelets denoise algorithm
//  Adapted from Gimp wavelets plugin (and ultimately DCraw by Dave Coffin).
//  fimg[0][rows][cols] = one color of image to denoise
//  fimg[1] and [2] = working space
//  thresh (0-10) is the adjustable parameter

void denoise_wavelets(float *fimg[3], uint ww2, uint hh2, float thresh)
{
   void denoise_wavelets_avgpix(float *temp, float *fimg, int st, int size, int sc);

   float    *temp, thold, stdev[5];
   uint     ii, lev, lpass, hpass, size, col, row;
   uint     samples[5];

   size = ww2 * hh2;
   temp = (float *) zmalloc ((ww2 + hh2) * sizeof(float));
   hpass = 0;

   for (lev = 0; lev < 5; lev++)
   {
      lpass = ((lev & 1) + 1);                                                   //  1, 2, 1, 2, 1

      for (row = 0; row < hh2; row++)                                            //  average row pixels
      {
         denoise_wavelets_avgpix(temp, fimg[hpass] + row * ww2, 1, ww2, 1 << lev);

         for (col = 0; col < ww2; col++)
            fimg[lpass][row * ww2 + col] = temp[col];
      }

      for (col = 0; col < ww2; col++)                                            //  average column pixels
      {
         denoise_wavelets_avgpix(temp, fimg[lpass] + col, ww2, hh2, 1 << lev);

         for (row = 0; row < hh2; row++)
            fimg[lpass][row * ww2 + col] = temp[row];
      }

      thold = 5.0 / (1 << 6) * exp (-2.6 * sqrt (lev + 1)) * 0.8002 / exp (-2.6);

      stdev[0] = stdev[1] = stdev[2] = stdev[3] = stdev[4] = 0.0;
      samples[0] = samples[1] = samples[2] = samples[3] = samples[4] = 0;

      for (ii = 0; ii < size; ii++)
      {
         fimg[hpass][ii] -= fimg[lpass][ii];

         if (fimg[hpass][ii] < thold && fimg[hpass][ii] > -thold)
         {
            if (fimg[lpass][ii] > 0.8) {
               stdev[4] += fimg[hpass][ii] * fimg[hpass][ii];
               samples[4]++;
            }
            else if (fimg[lpass][ii] > 0.6) {
               stdev[3] += fimg[hpass][ii] * fimg[hpass][ii];
               samples[3]++;
            }
            else if (fimg[lpass][ii] > 0.4) {
               stdev[2] += fimg[hpass][ii] * fimg[hpass][ii];
               samples[2]++;
            }
            else if (fimg[lpass][ii] > 0.2) {
               stdev[1] += fimg[hpass][ii] * fimg[hpass][ii];
               samples[1]++;
            }
            else {
               stdev[0] += fimg[hpass][ii] * fimg[hpass][ii];
               samples[0]++;
            }
         }
      }

      stdev[0] = sqrt (stdev[0] / (samples[0] + 1));
      stdev[1] = sqrt (stdev[1] / (samples[1] + 1));
      stdev[2] = sqrt (stdev[2] / (samples[2] + 1));
      stdev[3] = sqrt (stdev[3] / (samples[3] + 1));
      stdev[4] = sqrt (stdev[4] / (samples[4] + 1));

      for (ii = 0; ii < size; ii++)                                              //  do thresholding
      {
         if (fimg[lpass][ii] > 0.8)
            thold = thresh * stdev[4];
         else if (fimg[lpass][ii] > 0.6)
            thold = thresh * stdev[3];
         else if (fimg[lpass][ii] > 0.4)
            thold = thresh * stdev[2];
         else if (fimg[lpass][ii] > 0.2)
            thold = thresh * stdev[1];
         else
            thold = thresh * stdev[0];

         if (fimg[hpass][ii] < -thold)
            fimg[hpass][ii] += thold;
         else if (fimg[hpass][ii] > thold)
            fimg[hpass][ii] -= thold;
         else
            fimg[hpass][ii] = 0;

         if (hpass) fimg[0][ii] += fimg[hpass][ii];
      }

      hpass = lpass;
   }

   for (ii = 0; ii < size; ii++)
      fimg[0][ii] = fimg[0][ii] + fimg[lpass][ii];

   zfree(temp);
   return;
}


//  average pixels in one column or row
//  st = row stride (row length) or column stride (1)
//  sc = 1, 2, 4, 8, 16 = pixels +/- from target pixel to average

void denoise_wavelets_avgpix(float *temp, float *fimg, int st, int size, int sc)
{
  int ii;

  for (ii = 0; ii < sc; ii++)
    temp[ii] = 0.25*(2*fimg[st*ii] + fimg[st*(sc-ii)] + fimg[st*(ii+sc)]);

  for (NOP; ii < size - sc; ii++)
    temp[ii] = 0.25*(2*fimg[st*ii] + fimg[st*(ii-sc)] + fimg[st*(ii+sc)]);

  for (NOP; ii < size; ii++)
    temp[ii] = 0.25*(2*fimg[st*ii] + fimg[st*(ii-sc)] + fimg[st*(2*size-2-(ii+sc))]);

  return;
}


//  dialog to measure noise at mouse position

void denoise_measure()
{
   using namespace denoise_names;

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

   GtkWidget   *frdraw, *drawwin;
   char        text[100];
   cchar       *title = E2X("Measure Noise");
   cchar       *mousemess = E2X("Move mouse in a monotone image area.");

/***
          _______________________________________
         |           Measure Noise               |
         |                                       |
         |  Move mouse in a monotone image area. |
         | _____________________________________ |
         ||                                     ||
         ||                                     ||
         ||.....................................||
         ||                                     ||
         ||                                     ||
         ||_____________________________________||       drawing area
         ||                                     ||
         ||                                     ||
         ||.....................................||
         ||                                     ||
         ||_____________________________________||
         | center                           edge |
         |                                       |
         |   mean RGB:   100   150   200         |
         |  mean noise:  1.51  1.23  0.76        |
         |                                       |
         |                              [cancel] |
         |_______________________________________|

***/

   if (zd_denoise_measure) return;

   zdialog *zd = zdialog_new(title,Mwin,Bcancel,null);                           //  measure noise dialog
   zd_denoise_measure = zd;

   zdialog_add_widget(zd,"label","clab","dialog",mousemess,"space=5");

   zdialog_add_widget(zd,"frame","frdraw","dialog",0,"expand");                  //  frame for drawing areas
   frdraw = zdialog_widget(zd,"frdraw");
   drawwin = gtk_drawing_area_new();                                             //  drawing area
   gtk_container_add(GTK_CONTAINER(frdraw),drawwin);
   denoise_measure_drawwin = drawwin;
   
   zdialog_add_widget(zd,"hbox","hbce","dialog");
   zdialog_add_widget(zd,"label","labcen","hbce",Bcenter,"space=3");
   zdialog_add_widget(zd,"label","space","hbce",0,"expand");
   zdialog_add_widget(zd,"label","labend","hbce",Bedge,"space=5");

   snprintf(text,100,mformat,0.0,0.0,0.0);                                       //  mean RGB:     0     0     0
   zdialog_add_widget(zd,"label","mlab","dialog",text);
   snprintf(text,100,nformat,0.0,0.0,0.0);                                       //  mean noise:  0.00  0.00  0.00
   zdialog_add_widget(zd,"label","nlab","dialog",text);

   zdialog_resize(zd,300,300);
   zdialog_run(zd,denoise_measure_dialog_event,"save");                          //  run dialog
   return;
}


//  dialog event and completion function

int denoise_measure_dialog_event(zdialog *zd, cchar *event)
{
   using namespace denoise_names;

   void denoise_measure_mousefunc();
   
   if (strmatch(event,"focus"))
      takeMouse(denoise_measure_mousefunc,dragcursor);                           //  connect mouse function
   
   if (zd->zstat) {
      freeMouse();                                                               //  free mouse
      zdialog_free(zd);
      zd_denoise_measure = 0;
   }
   
   return 1;
}


//  mouse function
//  sample noise where the mouse is clicked
//  assumed: mouse is on a monotone image area

void denoise_measure_mousefunc()
{
   using namespace denoise_names;

   GtkWidget   *drawwin;
   GdkWindow   *gdkwin;
   cairo_t     *cr;
   zdialog     *zd = zd_denoise_measure;

   char        text[100];
   int         mx, my, px, py, qx, qy, Npix;
   float       *pix3, R;
   float       Rm, Gm, Bm, Rn, Gn, Bn, Ro, Go, Bo;
   int         dww, dhh;
   float       max, xscale, yscale;
   float       rx, ry;
   float       Noise[400][3];
   double      dashes[2] = { 1, 3 };
   
   if (! E3pxm) return;
   if (! zd) return;
   if (Fthreadbusy) return;

   mx = Mxposn;                                                                  //  mouse position
   my = Myposn;

   if (mx < 13 || mx >= E3ww-13) return;                                         //  must be 12+ pixels from image edge
   if (my < 13 || my >= E3hh-13) return;
   
   draw_mousecircle(Mxposn,Myposn,10,0,0);                                       //  draw mouse circle, radius 10

   Npix = 0;
   Rm = Gm = Bm = 0;
   
   for (py = my-10; py <= my+10; py++)                                           //  loop pixels within mouise circle
   for (px = mx-10; px <= mx+10; px++)                                           //  (approx. 314 pixels)
   {
      R = sqrtf((px-mx)*(px-mx) + (py-my)*(py-my));                              //  distance from center
      if (R > 10) continue;

      pix3 = PXMpix(E3pxm,px,py);                                                //  get pixel RGB values
      Rm += pix3[0];                                                             //  accumulate
      Gm += pix3[1];
      Bm += pix3[2];

      Npix++;
   }

   Rm = Rm / Npix;                                                               //  mean RGB values
   Gm = Gm / Npix;                                                               //    for pixels within mouse
   Bm = Bm / Npix;

   Npix = 0;
   Rn = Gn = Bn = 0;

   for (py = my-10; py <= my+10; py++)                                           //  loop pixels within mouise circle
   for (px = mx-10; px <= mx+10; px++)                                           //  (approx. 314 pixels)
   {
      R = sqrtf((px-mx)*(px-mx) + (py-my)*(py-my));                              //  distance from center
      if (R > 10) continue;

      Ro = Go = Bo = 0;

      for (qy = py-2; qy <= py+2; qy++)                                          //  for each pixel, get mean RGB
      for (qx = px-2; qx <= px+2; qx++)                                          //    for 5x5 surrounding pixels
      {
         pix3 = PXMpix(E3pxm,qx,qy);
         Ro += pix3[0];
         Go += pix3[1];
         Bo += pix3[2];
      }
      
      Ro = Ro / 25;                                                              //  mean RGB for surrounding pixels
      Go = Go / 25;
      Bo = Bo / 25;

      pix3 = PXMpix(E3pxm,px,py);                                                //  get pixel RGB noise levels

      Noise[Npix][0] = pix3[0] - Ro;                                             //  noise = pixel value - mean
      Noise[Npix][1] = pix3[1] - Go;
      Noise[Npix][2] = pix3[2] - Bo;
      
      Rn += fabsf(Noise[Npix][0]);                                               //  accumulate absolute values
      Gn += fabsf(Noise[Npix][1]);
      Bn += fabsf(Noise[Npix][2]);

      Npix++;
   }

   Rn = Rn / Npix;                                                               //  mean RGB noise levels 
   Gn = Gn / Npix;                                                               //    for pixels within mouse
   Bn = Bn / Npix;

   snprintf(text,100,mformat,0.0,0.0,0.0);                                       //  clear dialog data
   zdialog_stuff(zd,"mlab",text);
   snprintf(text,100,nformat,0.0,0.0,0.0);
   zdialog_stuff(zd,"nlab",text);

   snprintf(text,100,mformat,Rm,Gm,Bm);                                          //  mean RGB:   NNN   NNN   NNN
   zdialog_stuff(zd,"mlab",text);

   snprintf(text,100,nformat,Rn,Gn,Bn);                                          //  mean noise:  N.NN  N.NN  N.NN
   zdialog_stuff(zd,"nlab",text);

   max = Rn;
   if (Gn > max) max = Gn;
   if (Bn > max) max = Bn;

   drawwin = denoise_measure_drawwin;
   gdkwin = gtk_widget_get_window(drawwin);                                      //  GDK drawing window

   dww = gtk_widget_get_allocated_width(drawwin);                                //  drawing window size
   dhh = gtk_widget_get_allocated_height(drawwin);

   xscale = dww / 10.0;                                                          //  x scale:  0 to max radius
   yscale = dhh / 20.0;                                                          //  y scale: -10 to +10 noise level
   
   cr = draw_context_create(gdkwin,draw_context);
   
   cairo_set_source_rgb(cr,1,1,1);                                               //  white background
   cairo_paint(cr);
   
   cairo_set_source_rgb(cr,0,0,0);                                               //  paint black

   cairo_set_line_width(cr,2);                                                   //  center line
   cairo_set_dash(cr,dashes,0,0);
   cairo_move_to(cr,0,0.5*dhh);
   cairo_line_to(cr,dww,0.5*dhh);
   cairo_stroke(cr);
   
   cairo_set_dash(cr,dashes,2,0);                                                //  dash lines at -5 and +5
   cairo_move_to(cr,0,0.25*dhh);
   cairo_line_to(cr,dww,0.25*dhh);
   cairo_move_to(cr,0,0.75*dhh);
   cairo_line_to(cr,dww,0.75*dhh);
   cairo_stroke(cr);

   cairo_set_source_rgb(cr,1,0,0);

   Npix = 0;

   for (py = my-10; py <= my+10; py++)                                           //  loop pixels within mouise circle
   for (px = mx-10; px <= mx+10; px++)                                           //  (approx. 314 pixels)
   {
      R = sqrtf((px-mx)*(px-mx) + (py-my)*(py-my));                              //  distance from center
      if (R > 10) continue;
      Rn = Noise[Npix][0];                                                       //  RED noise
      rx = R * xscale;                                                           //  px, 0 to dww
      ry = 0.5 * dhh - Rn * yscale;                                              //  red py, 0 to dhh
      cairo_move_to(cr,rx,ry);
      cairo_arc(cr,rx,ry,1,0,2*PI);
      Npix++;
   }      

   cairo_stroke(cr);

   cairo_set_source_rgb(cr,0,1,0);

   Npix = 0;

   for (py = my-10; py <= my+10; py++)                                           //  same for GREEN noise
   for (px = mx-10; px <= mx+10; px++)
   {
      R = sqrtf((px-mx)*(px-mx) + (py-my)*(py-my));
      if (R > 10) continue;
      Gn = Noise[Npix][1];
      rx = R * xscale;
      ry = 0.5 * dhh - Gn * yscale;
      cairo_move_to(cr,rx,ry);
      cairo_arc(cr,rx,ry,1,0,2*PI);
      Npix++;
   }      

   cairo_stroke(cr);

   cairo_set_source_rgb(cr,0,0,1);

   Npix = 0;

   for (py = my-10; py <= my+10; py++)                                           //  same for BLUE noise
   for (px = mx-10; px <= mx+10; px++)
   {
      R = (px-mx)*(px-mx) + (py-my)*(py-my);
      if (R > 100) continue;
      R = 0.1 * R;
      Bn = Noise[Npix][2];
      rx = R * xscale;
      ry = 0.5 * dhh - Bn * yscale;
      cairo_move_to(cr,rx,ry);
      cairo_arc(cr,rx,ry,1,0,2*PI);
      Npix++;
   }      

   cairo_stroke(cr);

   draw_context_destroy(draw_context); 
   return;
}


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

//  red eye removal function

namespace redeye_names
{
   struct sredmem {                                                              //  red-eye struct in memory
      char        type, space[3];
      int         cx, cy, ww, hh, rad, clicks;
      float       thresh, tstep;
   };
   sredmem  redmem[100];                                                         //  store up to 100 red-eyes

   int      E3ww, E3hh;
   int      Nredmem = 0, maxredmem = 100;

   editfunc    EFredeye;

   #define pixred(pix) (25*pix[0]/(pixbright(pix)+1))                            //  red brightness 0-100%
}


//  menu function

void m_redeyes(GtkWidget *, cchar *)
{
   using namespace redeye_names;

   int      redeye_dialog_event(zdialog *zd, cchar *event);
   void     redeye_mousefunc();

   cchar    *redeye_message = E2X(
               "Method 1:\n"
               "  Left-click on red-eye to darken.\n"
               "Method 2:\n"
               "  Drag down and right to enclose red-eye.\n"
               "  Left-click on red-eye to darken.\n"
               "Undo red-eye:\n"
               "  Right-click on red-eye.");

   F1_help_topic = "red eyes";

   EFredeye.menufunc = m_redeyes;
   EFredeye.funcname = "redeyes";
   EFredeye.Farea = 1;                                                           //  select area ignored
   EFredeye.mousefunc = redeye_mousefunc;
   if (! edit_setup(EFredeye)) return;                                           //  setup edit
   
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;

   zdialog *zd = zdialog_new(E2X("Red Eye Reduction"),Mwin,Bdone,Bcancel,null);
   EFredeye.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",redeye_message);
   zdialog_run(zd,redeye_dialog_event,"save");                                   //  run dialog - parallel

   Nredmem = 0;
   takeMouse(redeye_mousefunc,dragcursor);                                       //  connect mouse function
   return;
}


//  dialog event and completion callback function

int redeye_dialog_event(zdialog *zd, cchar *event)
{
   using namespace redeye_names;

   void     redeye_mousefunc();

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   
   if (zd->zstat)
   {
      if (Nredmem > 0) {
         CEF->Fmods++;
         CEF->Fsaved = 0;
      }
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

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

   return 1;
}


int      redeye_createF(int px, int py);                                         //  create 1-click red-eye (type F)
int      redeye_createR(int px, int py, int ww, int hh);                         //  create robust red-eye (type R)
void     redeye_darken(int ii);                                                  //  darken red-eye
void     redeye_distr(int ii);                                                   //  build pixel redness distribution
int      redeye_find(int px, int py);                                            //  find red-eye at mouse position
void     redeye_remove(int ii);                                                  //  remove red-eye at mouse position
int      redeye_radlim(int cx, int cy);                                          //  compute red-eye radius limit

void redeye_mousefunc()
{
   using namespace redeye_names;

   int         ii, px, py, ww, hh;

   if (Nredmem == maxredmem) {
      zmessageACK(Mwin,"%d red-eye limit reached",maxredmem);                    //  too many red-eyes
      return;
   }

   if (LMclick)                                                                  //  left mouse click
   {
      px = Mxclick;                                                              //  click position
      py = Myclick;
      if (px < 0 || px > E3ww-1 || py < 0 || py > E3hh-1)                        //  outside image area
         return;

      ii = redeye_find(px,py);                                                   //  find existing red-eye
      if (ii < 0) ii = redeye_createF(px,py);                                    //  or create new type F
      redeye_darken(ii);                                                         //  darken red-eye
      Fpaint2();
   }

   if (RMclick)                                                                  //  right mouse click
   {
      px = Mxclick;                                                              //  click position
      py = Myclick;
      ii = redeye_find(px,py);                                                   //  find red-eye
      if (ii >= 0) redeye_remove(ii);                                            //  if found, remove
      Fpaint2();
   }

   LMclick = RMclick = 0;

   if (Mxdrag || Mydrag)                                                         //  mouse drag underway
   {
      px = Mxdown;                                                               //  initial position
      py = Mydown;
      ww = Mxdrag - Mxdown;                                                      //  increment
      hh = Mydrag - Mydown;
      Mxdrag = Mydrag = 0;
      if (ww < 2 && hh < 2) return;
      if (ww < 2) ww = 2;
      if (hh < 2) hh = 2;
      if (px < 1) px = 1;                                                        //  keep within image area
      if (py < 1) py = 1;
      if (px + ww > E3ww-1) ww = E3ww-1 - px;
      if (py + hh > E3hh-1) hh = E3hh-1 - py;
      ii = redeye_find(px,py);                                                   //  find existing red-eye
      if (ii >= 0) redeye_remove(ii);                                            //  remove it
      ii = redeye_createR(px,py,ww,hh);                                          //  create new red-eye type R
   }

   return;
}


//  create type F redeye (1-click automatic)

int redeye_createF(int cx, int cy)
{
   using namespace redeye_names;

   int         cx0, cy0, cx1, cy1, px, py, rad, radlim;
   int         loops, ii;
   int         Tnpix, Rnpix, R2npix;
   float       rd, rcx, rcy, redpart;
   float       Tsum, Rsum, R2sum, Tavg, Ravg, R2avg;
   float       sumx, sumy, sumr;
   float       *ppix;

   cx0 = cx;
   cy0 = cy;

   for (loops = 0; loops < 8; loops++)
   {
      cx1 = cx;
      cy1 = cy;

      radlim = redeye_radlim(cx,cy);                                             //  radius limit (image edge)
      Tsum = Tavg = Ravg = Tnpix = 0;

      for (rad = 0; rad < radlim-2; rad++)                                       //  find red-eye radius from (cx,cy)
      {
         Rsum = Rnpix = 0;
         R2sum = R2npix = 0;

         for (py = cy-rad-2; py <= cy+rad+2; py++)
         for (px = cx-rad-2; px <= cx+rad+2; px++)
         {
            rd = sqrt((px-cx)*(px-cx) + (py-cy)*(py-cy));
            ppix = PXMpix(E3pxm,px,py);
            redpart = pixred(ppix);

            if (rd <= rad + 0.5 && rd > rad - 0.5) {                             //  accum. redness at rad
               Rsum += redpart;
               Rnpix++;
            }
            else if (rd <= rad + 2.5 && rd > rad + 1.5) {                        //  accum. redness at rad+2
               R2sum += redpart;
               R2npix++;
            }
         }

         Tsum += Rsum;
         Tnpix += Rnpix;
         Tavg = Tsum / Tnpix;                                                    //  avg. redness over 0-rad
         Ravg = Rsum / Rnpix;                                                    //  avg. redness at rad
         R2avg = R2sum / R2npix;                                                 //  avg. redness at rad+2
         if (R2avg > Ravg || Ravg > Tavg) continue;
         if ((Ravg - R2avg) < 0.2 * (Tavg - Ravg)) break;                        //  0.1 --> 0.2
      }

      sumx = sumy = sumr = 0;
      rad = int(1.2 * rad + 1);
      if (rad > radlim) rad = radlim;

      for (py = cy-rad; py <= cy+rad; py++)                                      //  compute center of gravity for
      for (px = cx-rad; px <= cx+rad; px++)                                      //   pixels within rad of (cx,cy)
      {
         rd = sqrt((px-cx)*(px-cx) + (py-cy)*(py-cy));
         if (rd > rad + 0.5) continue;
         ppix = PXMpix(E3pxm,px,py);
         redpart = pixred(ppix);                                                 //  weight by redness
         sumx += redpart * (px - cx);
         sumy += redpart * (py - cy);
         sumr += redpart;
      }

      rcx = cx + 1.0 * sumx / sumr;                                              //  new center of red-eye
      rcy = cy + 1.0 * sumy / sumr;
      if (fabsf(cx0 - rcx) > 0.6 * rad) break;                                   //  give up if big movement
      if (fabsf(cy0 - rcy) > 0.6 * rad) break;
      cx = int(rcx + 0.5);
      cy = int(rcy + 0.5);
      if (cx == cx1 && cy == cy1) break;                                         //  done if no change
   }

   radlim = redeye_radlim(cx,cy);
   if (rad > radlim) rad = radlim;

   ii = Nredmem++;                                                               //  add red-eye to memory
   redmem[ii].type = 'F';
   redmem[ii].cx = cx;
   redmem[ii].cy = cy;
   redmem[ii].rad = rad;
   redmem[ii].clicks = 0;
   redmem[ii].thresh = 0;
   return ii;
}


//  create type R red-eye (drag an ellipse over red-eye area)

int redeye_createR(int cx, int cy, int ww, int hh)
{
   using namespace redeye_names;

   int      rad, radlim;

   draw_mousearc(cx,cy,2*ww,2*hh,0,0);                                           //  draw ellipse around mouse pointer

   if (ww > hh) rad = ww;
   else rad = hh;
   radlim = redeye_radlim(cx,cy);
   if (rad > radlim) rad = radlim;

   int ii = Nredmem++;                                                           //  add red-eye to memory
   redmem[ii].type = 'R';
   redmem[ii].cx = cx;
   redmem[ii].cy = cy;
   redmem[ii].ww = 2 * ww;
   redmem[ii].hh = 2 * hh;
   redmem[ii].rad = rad;
   redmem[ii].clicks = 0;
   redmem[ii].thresh = 0;
   return ii;
}


//  darken a red-eye and increase click count

void redeye_darken(int ii)
{
   using namespace redeye_names;

   int         cx, cy, ww, hh, px, py, rad, clicks;
   float       rd, thresh, tstep;
   char        type;
   float       *ppix;

   type = redmem[ii].type;
   cx = redmem[ii].cx;
   cy = redmem[ii].cy;
   ww = redmem[ii].ww;
   hh = redmem[ii].hh;
   rad = redmem[ii].rad;
   thresh = redmem[ii].thresh;
   tstep = redmem[ii].tstep;
   clicks = redmem[ii].clicks++;

   if (thresh == 0)                                                              //  1st click
   {
      redeye_distr(ii);                                                          //  get pixel redness distribution
      thresh = redmem[ii].thresh;                                                //  initial redness threshold
      tstep = redmem[ii].tstep;                                                  //  redness step size
      draw_mousearc(0,0,0,0,1,0);                                                //  erase mouse ellipse
   }

   tstep = (thresh - tstep) / thresh;                                            //  convert to reduction factor
   thresh = thresh * pow(tstep,clicks);                                          //  reduce threshold by total clicks

   for (py = cy-rad; py <= cy+rad; py++)                                         //  darken pixels over threshold
   for (px = cx-rad; px <= cx+rad; px++)
   {
      if (type == 'R') {
         if (px < cx - ww/2) continue;
         if (px > cx + ww/2) continue;
         if (py < cy - hh/2) continue;
         if (py > cy + hh/2) continue;
      }
      rd = sqrt((px-cx)*(px-cx) + (py-cy)*(py-cy));
      if (rd > rad + 0.5) continue;
      ppix = PXMpix(E3pxm,px,py);                                                //  set redness = threshold
      if (pixred(ppix) > thresh)
         ppix[0] = int(thresh * (0.65 * ppix[1] + 0.10 * ppix[2] + 1) / (25 - 0.25 * thresh));
   }

   return;
}


//  Build a distribution of redness for a red-eye. Use this information
//  to set initial threshold and step size for stepwise darkening.

void redeye_distr(int ii)
{
   using namespace redeye_names;

   int         cx, cy, ww, hh, rad, px, py;
   int         bin, npix, dbins[20], bsum, blim;
   float       rd, maxred, minred, redpart, dbase, dstep;
   char        type;
   float       *ppix;

   type = redmem[ii].type;
   cx = redmem[ii].cx;
   cy = redmem[ii].cy;
   ww = redmem[ii].ww;
   hh = redmem[ii].hh;
   rad = redmem[ii].rad;

   maxred = 0;
   minred = 100;

   for (py = cy-rad; py <= cy+rad; py++)
   for (px = cx-rad; px <= cx+rad; px++)
   {
      if (type == 'R') {
         if (px < cx - ww/2) continue;
         if (px > cx + ww/2) continue;
         if (py < cy - hh/2) continue;
         if (py > cy + hh/2) continue;
      }
      rd = sqrt((px-cx)*(px-cx) + (py-cy)*(py-cy));
      if (rd > rad + 0.5) continue;
      ppix = PXMpix(E3pxm,px,py);
      redpart = pixred(ppix);
      if (redpart > maxred) maxred = redpart;
      if (redpart < minred) minred = redpart;
   }

   dbase = minred;
   dstep = (maxred - minred) / 19.99;

   for (bin = 0; bin < 20; bin++) dbins[bin] = 0;
   npix = 0;

   for (py = cy-rad; py <= cy+rad; py++)
   for (px = cx-rad; px <= cx+rad; px++)
   {
      if (type == 'R') {
         if (px < cx - ww/2) continue;
         if (px > cx + ww/2) continue;
         if (py < cy - hh/2) continue;
         if (py > cy + hh/2) continue;
      }
      rd = sqrt((px-cx)*(px-cx) + (py-cy)*(py-cy));
      if (rd > rad + 0.5) continue;
      ppix = PXMpix(E3pxm,px,py);
      redpart = pixred(ppix);
      bin = int((redpart - dbase) / dstep);
      ++dbins[bin];
      ++npix;
   }

   bsum = 0;
   blim = int(0.5 * npix);

   for (bin = 0; bin < 20; bin++)                                                //  find redness level for 50% of
   {                                                                             //    pixels within red-eye radius
      bsum += dbins[bin];
      if (bsum > blim) break;
   }

   redmem[ii].thresh = dbase + dstep * bin;                                      //  initial redness threshold
   redmem[ii].tstep = dstep;                                                     //  redness step (5% of range)

   return;
}


//  find a red-eye (nearly) overlapping the mouse click position

int redeye_find(int cx, int cy)
{
   using namespace redeye_names;

   for (int ii = 0; ii < Nredmem; ii++)
   {
      if (cx > redmem[ii].cx - 2 * redmem[ii].rad &&
          cx < redmem[ii].cx + 2 * redmem[ii].rad &&
          cy > redmem[ii].cy - 2 * redmem[ii].rad &&
          cy < redmem[ii].cy + 2 * redmem[ii].rad)
            return ii;                                                           //  found
   }
   return -1;                                                                    //  not found
}


//  remove a red-eye from memory

void redeye_remove(int ii)
{
   using namespace redeye_names;

   int      cx, cy, rad, px, py;
   float    *pix1, *pix3;
   int      nc = E1pxm->nc, pcc = nc * sizeof(float);

   cx = redmem[ii].cx;
   cy = redmem[ii].cy;
   rad = redmem[ii].rad;

   for (px = cx-rad; px <= cx+rad; px++)
   for (py = cy-rad; py <= cy+rad; py++)
   {
      pix1 = PXMpix(E1pxm,px,py);
      pix3 = PXMpix(E3pxm,px,py);
      memcpy(pix3,pix1,pcc);
   }

   for (ii++; ii < Nredmem; ii++)
      redmem[ii-1] = redmem[ii];
   Nredmem--;

   draw_mousearc(0,0,0,0,1,0);                                                   //  erase mouse ellipse
   return;
}


//  compute red-eye radius limit: smaller of 100 and nearest image edge

int redeye_radlim(int cx, int cy)
{
   using namespace redeye_names;

   int radlim = 100;
   if (cx < 100) radlim = cx;
   if (E3ww-1 - cx < 100) radlim = E3ww-1 - cx;
   if (cy < 100) radlim = cy;
   if (E3hh-1 - cy < 100) radlim = E3hh-1 - cy;
   return radlim;
}


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

//  match_colors edit function
//  Adjust colors of image 2 to match the colors of image 1
//  using small selected areas in each image as the match standard.

namespace match_colors_names
{
   float    match_colors_RGB1[3];                                                //  image 1 base colors to match
   float    match_colors_RGB2[3];                                                //  image 2 target colors to match
   int      match_colors_radius = 10;                                            //  mouse radius
   int      match_colors_mode = 0;
   int      E3ww, E3hh;

   editfunc    EFmatchcolors;
}


//  menu function

void m_match_colors(GtkWidget *, const char *)
{
   using namespace match_colors_names;

   int    match_colors_dialog_event(zdialog* zd, const char *event);
   void * match_colors_thread(void *);
   void   match_colors_mousefunc();

   cchar    *title = E2X("Color Match Images");

   F1_help_topic = "match colors";
   if (checkpend("all")) return;                                                 //  check, no block: edit_setup() follows

/***
          ____________________________________________
         |       Color Match Images                   |
         |                                            |
         | 1  [ 10 ]   mouse radius for color sample  |
         | 2  [Open]   image for source color         |
         | 3  click on image to get source color      |
         | 4  [Open]   image for target color         |
         | 5  click on image to set target color      |
         |                                            |
         |                            [done] [cancel] |
         |____________________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);                     //  match_colors dialog
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=2");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=3");
   zdialog_add_widget(zd,"label","labn1","vb1","1");
   zdialog_add_widget(zd,"label","labn2","vb1","2");
   zdialog_add_widget(zd,"label","labn3","vb1","3");
   zdialog_add_widget(zd,"label","labn4","vb1","4");
   zdialog_add_widget(zd,"label","labn5","vb1","5");
   zdialog_add_widget(zd,"hbox","hbrad","vb2");
   zdialog_add_widget(zd,"zspin","radius","hbrad","1|20|1|10","space=5");
   zdialog_add_widget(zd,"label","labrad","hbrad",E2X("mouse radius for color sample"));
   zdialog_add_widget(zd,"hbox","hbop1","vb2");
   zdialog_add_widget(zd,"button","open1","hbop1",E2X("Open"),"space=5");
   zdialog_add_widget(zd,"label","labop1","hbop1",E2X("image for source color"));
   zdialog_add_widget(zd,"hbox","hbclik1","vb2");
   zdialog_add_widget(zd,"label","labclik1","hbclik1",E2X("click on image to get source color"));
   zdialog_add_widget(zd,"hbox","hbop2","vb2");
   zdialog_add_widget(zd,"button","open2","hbop2",E2X("Open"),"space=5");
   zdialog_add_widget(zd,"label","labop2","hbop2",E2X("image to set matching color"));
   zdialog_add_widget(zd,"hbox","hbclik2","vb2");
   zdialog_add_widget(zd,"label","labclik2","hbclik2",E2X("click on image to set matching color"));

   zdialog_stuff(zd,"radius",match_colors_radius);                               //  remember last radius

   EFmatchcolors.funcname = "match_colors";
   EFmatchcolors.Farea = 1;                                                      //  select area ignored
   EFmatchcolors.zd = zd;
   EFmatchcolors.threadfunc = match_colors_thread;
   EFmatchcolors.mousefunc = match_colors_mousefunc;

   match_colors_mode = 0;
   if (curr_file) {
      match_colors_mode = 1;                                                     //  image 1 ready to click
      takeMouse(match_colors_mousefunc,0);                                       //  connect mouse function
   }

   zdialog_run(zd,match_colors_dialog_event,"parent");                           //  run dialog - parallel
   return;
}


//  match_colors dialog event and completion function

int match_colors_dialog_event(zdialog *zd, const char *event)
{
   using namespace match_colors_names;

   void   match_colors_mousefunc();

   int      err;
   char     *file;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   
   if (zd->zstat)
   {
      if (match_colors_mode == 4) {                                              //  edit was started
         if (zd->zstat == 1) edit_done(0);                                       //  commit edit
         else edit_cancel(0);                                                    //  discard edit
         match_colors_mode = 0;
         return 1;
      }

      freeMouse();                                                               //  abandoned
      zdialog_free(zd);
      match_colors_mode = 0;
      return 1;
   }

   if (strmatch(event,"radius"))                                                 //  set new mouse radius
      zdialog_fetch(zd,"radius",match_colors_radius);

   if (strmatch(event,"open1"))                                                  //  get image 1 for color source
   {
      if (match_colors_mode == 4) edit_cancel(1);                                //  cancel edit, keep dialog
      match_colors_mode = 0;
      file = gallery_select1(null);                                              //  open image 1
      if (file) {
         err = f_open(file);
         if (! err) match_colors_mode = 1;                                       //  image 1 ready to click
      }
   }

   if (strmatch(event,"open2"))                                                  //  get image 2 to set matching color
   {
      if (match_colors_mode < 2) {
         zmessageACK(Mwin,E2X("select source image color first"));               //  check that RGB1 has been set
         return 1;
      }
      match_colors_mode = 2;
      file = gallery_select1(null);                                              //  open image 2
      if (! file) return 1;
      err = f_open(file);
      if (err) return 1;
      match_colors_mode = 3;                                                     //  image 2 ready to click
   }

   takeMouse(match_colors_mousefunc,0);                                          //  reconnect mouse function
   return 1;
}


//  mouse function - click on image and get colors to match

void match_colors_mousefunc()
{
   using namespace match_colors_names;

   void  match_colors_getRGB(int px, int py, float rgb[3]);

   int      px, py;

   if (match_colors_mode < 1) return;                                            //  no image available yet

   draw_mousecircle(Mxposn,Myposn,match_colors_radius,0,0);                      //  draw circle around pointer

   if (LMclick)
   {
      LMclick = 0;
      px = Mxclick;
      py = Myclick;

      if (match_colors_mode == 1 || match_colors_mode == 2)                      //  image 1 ready to click
      {
         match_colors_getRGB(px,py,match_colors_RGB1);                           //  get RGB1 color
         match_colors_mode = 2;
         return;
      }

      if (match_colors_mode == 3 || match_colors_mode == 4)                      //  image 2 ready to click
      {
         if (match_colors_mode == 4) edit_reset();
         else {
            if (! edit_setup(EFmatchcolors)) return;                             //  setup edit - thread will launch
            E3ww = E3pxm->ww;
            E3hh = E3pxm->hh;
            match_colors_mode = 4;                                               //  edit waiting for cancel or done
         }

         match_colors_getRGB(px,py,match_colors_RGB2);                           //  get RGB2 color
         signal_thread();                                                        //  update the target image
         return;
      }
   }

   return;
}


//  get the RGB averages for pixels within mouse radius

void match_colors_getRGB(int px, int py, float rgb[3])
{
   using namespace match_colors_names;

   int      radflat1 = match_colors_radius;
   int      radflat2 = radflat1 * radflat1;
   int      rad, npix, qx, qy;
   float    red, green, blue;
   float    *pix1;
   PXM      *pxm;

   pxm = PXM_load(curr_file,1);                                                  //  popup ACK if error
   if (! pxm) return;

   npix = 0;
   red = green = blue = 0;

   for (qy = py-radflat1; qy <= py+radflat1; qy++)
   for (qx = px-radflat1; qx <= px+radflat1; qx++)
   {
      if (qx < 0 || qx > pxm->ww-1) continue;
      if (qy < 0 || qy > pxm->hh-1) continue;
      rad = (qx-px) * (qx-px) + (qy-py) * (qy-py);
      if (rad > radflat2) continue;
      pix1 = PXMpix(pxm,qx,qy);
      red += pix1[0];
      green += pix1[1];
      blue += pix1[2];
      npix++;
   }

   rgb[0] = red / npix;
   rgb[1] = green / npix;
   rgb[2] = blue / npix;

   PXM_free(pxm);
   return;
}


//  thread function - start multiple working threads

void * match_colors_thread(void *)
{
   using namespace match_colors_names;

   void * match_colors_wthread(void *arg);

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

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

      do_wthreads(match_colors_wthread,NWT);                                     //  worker threads

      CEF->Fmods++;
      CEF->Fsaved = 0;

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

   return 0;                                                                     //  not executed, avoid warning
}


void * match_colors_wthread(void *arg)                                           //  worker thread function
{
   using namespace match_colors_names;

   int         index = *((int *) (arg));
   int         px, py;
   float       *pix3;
   float       Rred, Rgreen, Rblue;
   float       red, green, blue, cmax;

   Rred = match_colors_RGB1[0] / match_colors_RGB2[0];                           //  color adjustment ratios
   Rgreen = match_colors_RGB1[1] / match_colors_RGB2[1];
   Rblue = match_colors_RGB1[2] / match_colors_RGB2[2];

   for (py = index; py < E3hh; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < E3ww; px++)
   {
      pix3 = PXMpix(E3pxm,px,py);

      red = pix3[0] * Rred;                                                      //  adjust colors
      green = pix3[1] * Rgreen;
      blue = pix3[2] * Rblue;

      cmax = red;                                                                //  check for overflow
      if (green > cmax) cmax = green;
      if (blue > cmax) cmax = blue;

      if (cmax > 255.9) {                                                        //  fix overflow
         red = red * 255.9 / cmax;
         green = green * 255.9 / cmax;
         blue = blue * 255.9 / cmax;
      }

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

   pthread_exit(0);
}


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

//  Smart Erase menu function - Replace pixels inside a select area
//    with a reflection of pixels outside the area.

namespace smarterase_names
{
   editfunc    EFsmarterase;
   int         E3ww, E3hh;
}


//  menu function

void m_smart_erase(GtkWidget *, const char *)
{
   using namespace smarterase_names;

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

   cchar    *erase_message = E2X("Drag mouse to select. Erase. Repeat. \n"
                                 "Click: extend selection to mouse.");
   F1_help_topic = "smart erase";

   EFsmarterase.menufunc = m_smart_erase;
   EFsmarterase.funcname = "smart_erase";
   EFsmarterase.Farea = 0;                                                       //  select area deleted
   EFsmarterase.mousefunc = sa_mouse_select;                                     //  mouse function (use select area)
   if (! edit_setup(EFsmarterase)) return;                                       //  setup edit
   
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;

/***
       _________________________________________
      |                                         |
      | Drag mouse to select. Erase. Repeat.    |
      | Click: extend selection to mouse.       |
      |                                         |
      | Radius [ 10 ]    Blur [ 1.5 ]           |
      | [New Area] [Show] [Hide] [Erase] [Undo] |
      |                                         |
      |                                 [Done]  |
      |_________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Smart Erase"),Mwin,Bdone,null);
   EFsmarterase.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",erase_message,"space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labr","hb2",E2X("Radius"),"space=5");
   zdialog_add_widget(zd,"zspin","radius","hb2","1|30|1|10");
   zdialog_add_widget(zd,"label","labb","hb2",E2X("Blur"),"space=10");
   zdialog_add_widget(zd,"zspin","blur","hb2","0|9|0.5|1");
   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","newarea","hb3",E2X("New Area"),"space=3");
   zdialog_add_widget(zd,"button","show","hb3",E2X(Bshow),"space=3");
   zdialog_add_widget(zd,"button","hide","hb3",Bhide,"space=3");
   zdialog_add_widget(zd,"button","erase","hb3",Berase,"space=3");
   zdialog_add_widget(zd,"button","undo1","hb3",Bundo,"space=3");

   sa_clear();                                                                   //  clear area if any
   sa_pixmap_create();                                                           //  allocate select area pixel maps    19.0
   sa_mode = mode_mouse;                                                         //  mode = select by mouse
   sa_stat = 1;                                                                  //  status = active edit
   sa_fww = E1pxm->ww;
   sa_fhh = E1pxm->hh;
   sa_searchrange = 1;                                                           //  search within mouse radius
   sa_mouseradius = 10;                                                          //  initial mouse select radius
   sa_lastx = sa_lasty = 0;                                                      //  initz. for sa_mouse_select 
   takeMouse(sa_mouse_select,0);                                                 //  use select area mouse function
   sa_show(1,0);

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


//  dialog event and completion function

int smart_erase_dialog_event(zdialog *zd, const char *event)                     //  overhauled
{
   using namespace smarterase_names;

   void smart_erase_func(int mode);
   int  smart_erase_blur(float radius);

   float       radius;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat)
   {
      sa_clear();                                                                //  clear select area
      freeMouse();                                                               //  disconnect mouse
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strmatch(event,"radius"))
      zdialog_fetch(zd,"radius",sa_mouseradius);

   if (strmatch(event,"newarea")) {
      sa_clear();
      sa_lastx = sa_lasty = 0;                                                   //  forget last click                  19.0
      sa_pixmap_create();                                                        //  allocate select area pixel maps    19.0
      sa_mode = mode_mouse;                                                      //  mode = select by mouse
      sa_stat = 1;                                                               //  status = active edit
      sa_fww = E1pxm->ww;
      sa_fhh = E1pxm->hh;
      sa_show(1,0);
      takeMouse(sa_mouse_select,0);
   }

   if (strmatch(event,"show")) {
      sa_show(1,0);
      takeMouse(sa_mouse_select,0);
   }

   if (strmatch(event,"hide")) {
      sa_show(0,0);
      freeMouse();
   }

   if (strmatch(event,"erase")) {                                                //  do smart erase
      sa_finish_auto();                                                          //  finish the area
      smart_erase_func(1);
      zdialog_fetch(zd,"blur",radius);                                           //  add optional blur
      if (radius > 0) smart_erase_blur(radius);
      sa_show(0,0);
   }

   if (strmatch(event,"undo1"))                                                  //  dialog undo, undo last erase
      smart_erase_func(2);

   return 1;
}


//  erase the area or restore last erased area
//  mode = 1 = erase, mode = 2 = restore

void smart_erase_func(int mode)
{
   using namespace smarterase_names;

   int         px, py, npx, npy;
   int         qx, qy, sx, sy, tx, ty;
   int         ww, hh, ii, rad, inc, cc;
   int         dist2, mindist2;
   float       slope;
   char        *pmap;
   float       *pix1, *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   if (sa_stat != 3) return;                                                     //  nothing selected
   if (! sa_validate()) return;                                                  //  area invalid for curr. image file

   ww = E1pxm->ww;
   hh = E1pxm->hh;

   for (py = sa_miny; py < sa_maxy; py++)                                        //  undo all pixels in area
   for (px = sa_minx; px < sa_maxx; px++)
   {
      ii = py * ww + px;
      if (! sa_pixmap[ii]) continue;                                             //  pixel not selected

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      memcpy(pix3,pix1,pcc);
   }

   Fpaint2();                                                                    //  update window

   if (mode == 2) return;                                                        //  mode = undo, done

   cc = ww * hh;                                                                 //  allocate pixel done map
   pmap = (char *) zmalloc(cc);
   memset(pmap,0,cc);

   for (py = sa_miny; py < sa_maxy; py++)                                        //  loop all pixels in area
   for (px = sa_minx; px < sa_maxx; px++)
   {
      ii = py * ww + px;
      if (! sa_pixmap[ii]) continue;                                             //  pixel not selected
      if (pmap[ii]) continue;                                                    //  pixel already done

      mindist2 = 999999;                                                         //  find nearest edge
      npx = npy = 0;

      for (rad = 1; rad < 50; rad++)                                             //  50 pixel limit
      {
         for (qx = px-rad; qx <= px+rad; qx++)                                   //  search within rad
         for (qy = py-rad; qy <= py+rad; qy++)
         {
            if (qx < 0 || qx >= ww) continue;                                    //  off image edge
            if (qy < 0 || qy >= hh) continue;
            ii = qy * ww + qx;
            if (sa_pixmap[ii]) continue;                                         //  within selected area

            dist2 = (px-qx) * (px-qx) + (py-qy) * (py-qy);                       //  distance**2 to edge pixel
            if (dist2 < mindist2) {
               mindist2 = dist2;
               npx = qx;                                                         //  save nearest edge pixel found
               npy = qy;
            }
         }

         if (rad * rad >= mindist2) break;                                       //  found edge, can quit now
      }

      if (! npx && ! npy) continue;                                              //  edge not found, should not happen

      qx = npx;                                                                  //  nearest edge pixel from px/py
      qy = npy;

      if (abs(qy - py) > abs(qx - px))                                           //  line px/py to qx/qy is more
      {                                                                          //    vertical than horizontal
         slope = 1.0 * (qx - px) / (qy - py);
         if (qy > py) inc = 1;
         else inc = -1;

         for (sy = py; sy != qy; sy += inc) {                                    //  sx/sy = line from px/py to qx/qy
            sx = px + slope * (sy - py);

            ii = sy * ww + sx;
            if (pmap[ii]) continue;                                              //  skip done pixels
            pmap[ii] = 1;

            tx = qx + (qx - sx);                                                 //  tx/ty = extended line from qx/qy
            ty = qy + (qy - sy);

            if (tx < 0) tx = 0;                                                  //  don't go off edge
            if (tx > ww-1) tx = ww-1;
            if (ty < 0) ty = 0;
            if (ty > hh-1) ty = hh-1;

            pix1 = PXMpix(E1pxm,tx,ty);                                          //  copy pixel from tx/ty to sx/sy
            pix3 = PXMpix(E3pxm,sx,sy);                                          //  simplified
            memcpy(pix3,pix1,pcc);
         }
      }

      else                                                                       //  more horizontal than vertical
      {
         slope = 1.0 * (qy - py) / (qx - px);
         if (qx > px) inc = 1;
         else inc = -1;

         for (sx = px; sx != qx; sx += inc) {
            sy = py + slope * (sx - px);

            ii = sy * ww + sx;
            if (pmap[ii]) continue;
            pmap[ii] = 1;

            tx = qx + (qx - sx);
            ty = qy + (qy - sy);

            if (tx < 0) tx = 0;
            if (tx > ww-1) tx = ww-1;
            if (ty < 0) ty = 0;
            if (ty > hh-1) ty = hh-1;

            pix1 = PXMpix(E1pxm,tx,ty);
            pix3 = PXMpix(E3pxm,sx,sy);
            memcpy(pix3,pix1,pcc);
         }
      }
   }

   zfree(pmap);                                                                  //  free memory
   CEF->Fmods++;
   CEF->Fsaved = 0;

   Fpaint2();                                                                    //  update window
   return;
}


//  add blur to the erased area to help mask the side-effects

int smart_erase_blur(float radius)
{
   using namespace smarterase_names;

   int         ii, px, py, dx, dy, adx, ady;
   float       blur_weight[12][12];                                              //  up to blur radius = 10
   float       rad, radflat2;
   float       m, d, w, sum, weight;
   float       red, green, blue;
   float       *pix9, *pix3, *pixN;
   int         nc = E3pxm->nc;

   if (sa_stat != 3) return 0;

   rad = radius - 0.2;
   radflat2 = rad * rad;

   for (dx = 0; dx < 12; dx++)                                                   //  clear weights array
   for (dy = 0; dy < 12; dy++)
      blur_weight[dx][dy] = 0;

   for (dx = -rad-1; dx <= rad+1; dx++)                                          //  blur_weight[dx][dy] = no. of pixels
   for (dy = -rad-1; dy <= rad+1; dy++)                                          //    at distance (dx,dy) from center
      ++blur_weight[abs(dx)][abs(dy)];

   m = sqrt(radflat2 + radflat2);                                                //  corner pixel distance from center
   sum = 0;

   for (dx = 0; dx <= rad+1; dx++)                                               //  compute weight of pixel
   for (dy = 0; dy <= rad+1; dy++)                                               //    at distance dx, dy
   {
      d = sqrt(dx*dx + dy*dy);
      w = (m + 1.2 - d) / m;
      w = w * w;
      sum += blur_weight[dx][dy] * w;
      blur_weight[dx][dy] = w;
   }

   for (dx = 0; dx <= rad+1; dx++)                                               //  make weights add up to 1.0
   for (dy = 0; dy <= rad+1; dy++)
      blur_weight[dx][dy] = blur_weight[dx][dy] / sum;

   E9pxm = PXM_copy(E3pxm);                                                      //  copy edited image

   for (py = sa_miny; py < sa_maxy; py++)                                        //  loop all pixels in area
   for (px = sa_minx; px < sa_maxx; px++)
   {
      ii = py * E1pxm->ww + px;
      if (! sa_pixmap[ii]) continue;                                             //  pixel not in area

      pix9 = PXMpix(E9pxm,px,py);                                                //  source pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  target pixel

      rad = radius;
      red = green = blue = 0;

      for (dy = -rad-1; dy <= rad+1; dy++)                                       //  loop neighbor pixels within radius
      for (dx = -rad-1; dx <= rad+1; dx++)
      {
         if (px+dx < 0 || px+dx >= E3ww) continue;                               //  omit pixels off edge
         if (py+dy < 0 || py+dy >= E3hh) continue;
         adx = abs(dx);
         ady = abs(dy);
         pixN = pix9 + (dy * E3ww + dx) * nc;
         weight = blur_weight[adx][ady];                                         //  weight at distance (dx,dy)
         red += pixN[0] * weight;                                                //  accumulate contributions
         green += pixN[1] * weight;
         blue += pixN[2] * weight;
      }

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

   PXM_free(E9pxm);

   CEF->Fmods++;
   CEF->Fsaved = 0;

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


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

//  Shift R/B color planes to maximize overlap with G plane.
//  Radius of a pixel in R-plane or B-plane is shifted using the formula:
//    R2 = F1 * R1 + F2 * R1 * R1 + F3 * sqrt(R1) 
//    R1: pixel old radius   R2: pixel new radius
//    F1 F2 F3: computed values for maximum overlap
//    F1 is near 1.0
//    F2 and F3 are near 0.0

namespace chromatic1_names
{
   editfunc    EFchromatic1;
   double      Rf1, Rf2, Rf3, Bf1, Bf2, Bf3;
   int         Eww, Ehh;
   int         cx, cy;
   uint8       *pixcon;
   int         threshcon;
   int         evrgb;
   float       evF1, evF2, evF3;
   double      evRsum1[max_threads];
   double      evRsum2;
}


//  menu function

void m_chromatic1(GtkWidget *, cchar *menu)                                      //  20.0
{
   using namespace chromatic1_names;
   
   void  chromatic1_threshcon();
   int   chromatic1_dialog_event(zdialog* zd, cchar *event);
   
   cchar    *title = E2X("Chromatic Aberration");
   F1_help_topic = "chromatic 1";

   EFchromatic1.menuname = menu;                                                 //  setup edit
   EFchromatic1.menufunc = m_chromatic1;
   EFchromatic1.funcname = "chromatic1";
   EFchromatic1.Farea = 1;                                                       //  select area ignored
   if (! edit_setup(EFchromatic1)) return;

   Eww = E3pxm->ww;                                                              //  image dimensions
   Ehh = E3pxm->hh;

   cx = Eww / 2;                                                                 //  image center
   cy = Ehh / 2;
   
   chromatic1_threshcon();                                                       //  get image threshold contrast
   
/***
       ____________________________________________
      |          Chromatic Aberration              |
      |                                            |
      |  Red Factors:   [ 1.0 ]  [ 0.0 ]  [ 0.0 ]  |
      |  Blue Factors:  [ 1.0 ]  [ 0.0 ]  [ 0.0 ]  |
      |                                            |
      |  Find optimum factors: [ Search ]          |
      |                                            |
      |                            [Done] [Cancel] |
      |____________________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb4","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"label","labR","vb1",E2X("Red Factors"));
   zdialog_add_widget(zd,"zspin","Rf1","vb2","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"zspin","Rf2","vb3","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"zspin","Rf3","vb4","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"label","labB","vb1",E2X("Blue Factors"));
   zdialog_add_widget(zd,"zspin","Bf1","vb2","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"zspin","Bf2","vb3","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"zspin","Bf3","vb4","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"hbox","hbopt","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labopt","hbopt",E2X("Find optimum factors:"));
   zdialog_add_widget(zd,"button","search","hbopt",Bsearch,"space=5");

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


//  dialog event and completion function

int chromatic1_dialog_event(zdialog *zd, cchar *event)
{
   using namespace chromatic1_names;

   void  chromatic1_RBshift();
   void  chromatic1_RBsearch();

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                          //  done
      else edit_cancel(0);                                                       //  discard edit
      zfree(pixcon);                                                             //  free memory
      return 1;
   }

   if (zstrstr("Rf1 Rf2 Rf3 Bf1 Bf2 Bf3",event))
   {
      zdialog_fetch(zd,"Rf1",Rf1);                                               //  get manually adjusted factors
      zdialog_fetch(zd,"Rf2",Rf2);
      zdialog_fetch(zd,"Rf3",Rf3);
      zdialog_fetch(zd,"Bf1",Bf1);
      zdialog_fetch(zd,"Bf2",Bf2);
      zdialog_fetch(zd,"Bf3",Bf3);
      zmainloop();
      
      chromatic1_RBshift();                                                      //  shift R/B color planes using factors
   }
   
   if (strmatch(event,"search"))
   {
      chromatic1_RBsearch();                                                     //  search for optimum factors
      
      zdialog_stuff(zd,"Rf1",Rf1);                                               //  stuff dialog with found factors
      zdialog_stuff(zd,"Rf2",Rf2);
      zdialog_stuff(zd,"Rf3",Rf3);
      zdialog_stuff(zd,"Bf1",Bf1);
      zdialog_stuff(zd,"Bf2",Bf2);
      zdialog_stuff(zd,"Bf3",Bf3);

      chromatic1_RBshift();                                                      //  shift R/B color planes using factors
   }

   return 1;
}


//  Find contrast threshold for highest-contrast pixels in color green.

void chromatic1_threshcon()
{
   using namespace chromatic1_names;

   int      ii, jj;
   int      px, py, qx, qy;
   int      Gcon, Gmax;
   int      condist[256];
   float    *pix1, *pix2;

   pixcon = (uint8 *) zmalloc(Eww * Ehh);                                        //  map of pixel green contrast
   
   for (py = 9; py < Ehh-9; py++)                                                //  loop all pixels
   for (px = 9; px < Eww-9; px++)
   {
      Gmax = 0;
      pix1 = PXMpix(E1pxm,px,py);

      for (qy = py - 5; qy <= py + 5; qy += 10)                                  //  loop 4 neighbors offset by 5
      for (qx = px - 5; qx <= px + 5; qx += 10)
      {
         pix2 = PXMpix(E1pxm,qx,qy);                                             //  find max. green contrast
         Gcon = pix1[1] - pix2[1];
         Gcon = abs(Gcon);
         if (Gcon > Gmax) Gmax = Gcon;
      }
      
      ii = py * Eww + px;                                                        //  save pixel green contrast
      pixcon[ii] = Gmax;
   }
   
   memset(condist, 0, 256 * sizeof(int));

   for (ii = 0; ii < Eww * Ehh; ii++)                                            //  make histogram of contrast values
   {
      jj = pixcon[ii];                                                           //  condist[jj] = pixels with
      ++condist[jj];                                                             //    contrast value jj 
   }
   
   for (jj = 0, ii = 255; ii > 0; ii--)                                          //  find minimum contrast for 200K
   {                                                                             //    highest green-contrast pixels
      jj += condist[ii];
      if (ii > 100 && jj > 100000) break;
      if (jj > 200000) break;
   }

   threshcon = ii;                                                               //  threshold for chromatic1_evaluate()
   return;
}


//  Compute factors Rf1 Rf2 Bf1 Bf2 that minimize
//    Rplane - Gplane  and  Bplane - Gplane

void chromatic1_RBsearch()
{
   using namespace chromatic1_names;

   double chromatic1_evaluate(int rgb, float F1, float F2, float F3);

   int      rgb;                                                                 //  0/1/2 = red/green/blue
   float    F1, F2, F3;
   float    R1, R2, R3;
   float    S1, S2, S3;
   double   Rsum, Rmin;
   
   Ffuncbusy = 1;
   Fbusy_goal = 1372;

   for (rgb = 0; rgb <= 2; rgb += 2)                                             //  loop rgb = 0, 2  (red, blue) 
   {
      Rmin = 1.0 * Eww * Ehh * 256;                                              //  max. possible image difference
      R1 = R2 = R3 = 0;

      for (F1 = -3; F1 <= +3; F1 += 1.0)                                         //  loop all combinations F1, F2, F3
      for (F2 = -3; F2 <= +3; F2 += 1.0)                                         //    in 1-pixel steps
      for (F3 = -3; F3 <= +3; F3 += 1.0)
      {
         Rsum = chromatic1_evaluate(rgb, F1, F2, F3);                            //  evaluate each combination
         if (Rsum < Rmin) {
            Rmin = Rsum;                                                         //  remember best combination
            R1 = F1;
            R2 = F2;
            R3 = F3;
         }
         
         Fbusy_done++;
         zmainloop();
      }
      
      S1 = R1; S2 = R2; S3 = R3;                                                 //  loop around best combination
      
      for (F1 = R1-1; F1 <= R1+1; F1 += 0.333)                                   //  loop all combinations F1, F2, F3
      for (F2 = R2-1; F2 <= R2+1; F2 += 0.333)                                   //    in 0.333 pixel steps
      for (F3 = R3-1; F3 <= R3+1; F3 += 0.333)
      {
         Rsum = chromatic1_evaluate(rgb, F1, F2, F3);                            //  evaluate each combination
         if (Rsum < Rmin) {
            Rmin = Rsum;                                                         //  remember best combination
            S1 = F1;
            S2 = F2;
            S3 = F3;
         }

         Fbusy_done++;
         zmainloop();
      }
      
      if (rgb == 0) {
         Rf1 = S1;                                                               //  red plane factors
         Rf2 = S2;
         Rf3 = S3;
      }
      else {
         Bf1 = S1;                                                               //  blue plane factors
         Bf2 = S2;
         Bf3 = S3;
      }
   }
   
   Ffuncbusy = 0;
   Fbusy_goal = Fbusy_done = 0;
   return;
}


//  evaluate the alignment of a shifted R/B color plane with the G plane
//  R/B color plane is shifted using factors F1 F2 F3
//  rgb is 0/2 for R/B

double chromatic1_evaluate(int rgb, float F1, float F2, float F3)
{
   using namespace chromatic1_names;

   void * chromatic1_evaluate_wthread(void *);

   evrgb = rgb;                                                                  //  make args avail. for thread
   evF1 = F1;
   evF2 = F2;
   evF3 = F3;

   do_wthreads(chromatic1_evaluate_wthread,NWT);                                 //  do worker threads
   
   evRsum2 = 0;
   for (int ii = 0; ii < NWT; ii++) evRsum2 += evRsum1[ii];
   return evRsum2;
}


void * chromatic1_evaluate_wthread(void *arg)                                    //  worker thread
{
   using namespace chromatic1_names;

   int      index = *((int *) arg);
   int      ii, px1, py1;
   float    fx1, fy1, fx2, fy2, fx3, fy3;
   float    *pix1, vpix[4];
   double   Rsum = 0;
   
   for (py1 = index + 9; py1 < Ehh-9; py1 += NWT)                                //  loop all image pixels
   for (px1 = 9; px1 < Eww-9; px1++)
   {
      ii = py1 * Eww + px1;                                                      //  skip low contrast pixel
      if (pixcon[ii] < threshcon) continue;
      fx1 = 1.0 * (px1 - cx) / cx;                                               //  -1 to +1 at edges, 0 at center
      fy1 = 1.0 * (py1 - cy) / cy;
      fx2 = fx1 * fabsf(fx1);                                                    //  square, keep sign
      fy2 = fy1 * fabsf(fy1);
      fx3 = fx1 * fx2;                                                           //  cube
      fy3 = fy1 * fy2;
      fx1 = fx1 * evF1;                                                          //  * F1
      fy1 = fy1 * evF1;
      fx2 = fx2 * evF2;                                                          //  * F2
      fy2 = fy2 * evF2;
      fx3 = fx3 * evF3;                                                          //  * F3
      fy3 = fy3 * evF3;
      pix1 = PXMpix(E1pxm,px1,py1);                                              //  image unshifted pixel (G)
      vpixel(E1pxm, px1+fx1+fx2+fx3, py1+fy1+fy2+fy3, vpix);                     //  virtual pixel, shifted (R/B) 
      Rsum += fabs(vpix[evrgb] - pix1[1]);                                       //  sum shifted R/B - unshifted G
   }

   evRsum1[index] = Rsum;

   pthread_exit(0);
}

   
//  shift R/B color planes using factors Rf1 Rf2 Bf1 Bf2

void chromatic1_RBshift()
{
   void * chromatic1_RBshift_wthread(void *);

   do_wthreads(chromatic1_RBshift_wthread,NWT);                                  //  do worker threads

   CEF->Fmods++;                                                                 //  image is modified
   CEF->Fsaved = 0;                                                              //  and not saved

   Fpaint2();                                                                    //  update window
   return;
}


void * chromatic1_RBshift_wthread(void *arg)                                     //  worker thread
{
   using namespace chromatic1_names;

   int      index = *((int *) arg);
   int      px3, py3, ok;
   float    px1, py1;
   float    fx1, fy1, fx2, fy2, fx3, fy3;
   float    *pix3, vpix[4];
   
   for (py3 = index; py3 < Ehh; py3 += NWT)                                      //  loop all image pixels
   for (px3 = 0; px3 < Eww; px3++)
   {
      pix3 = PXMpix(E3pxm,px3,py3);                                              //  output pixel

      fx1 = 1.0 * (px3 - cx) / cx;                                               //  -1 to +1 at edges, 0 at center
      fy1 = 1.0 * (py3 - cy) / cy;
      fx2 = fx1 * fabsf(fx1);                                                    //  square, keep sign
      fy2 = fy1 * fabsf(fy1);
      fx3 = fx1 * fx2;                                                           //  cube
      fy3 = fy1 * fy2;

      px1 = px3 + Rf1 * fx1 + Rf2 * fx2 + Rf3 * fx3;                             //  red shift
      py1 = py3 + Rf1 * fy1 + Rf2 * fy2 + Rf3 * fy3;
      ok = vpixel(E1pxm,px1,py1,vpix);                                           //  red input pixel
      if (ok) pix3[0] = vpix[0]; 

      px1 = px3 + Bf1 * fx1 + Bf2 * fx2 + Bf3 * fx3;                             //  blue shift
      py1 = py3 + Bf1 * fy1 + Bf2 * fy2 + Bf3 * fy3;
      ok = vpixel(E1pxm,px1,py1,vpix);                                           //  blue input pixel
      if (ok) pix3[2] = vpix[2];
   }
   
   pthread_exit(0);
}


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

//  Remove axial chromatic aberration, color bands often seen on
//    dark image features against a bright background.

namespace chromatic2_names
{
   editfunc    EFchromatic2;
   zdialog     *zd;
   int         Eww, Ehh;                                                         //  image dimensions
   float       Crgb[3];                                                          //  chromatic color
   float       Rrgb[3];                                                          //  replacement color
   float       Brgb[3];                                                          //  background color
   float       Rhsl[3];                                                          //  replacement color, HSL space
   float       Cmatch;                                                           //  chromatic color match level
   int         pcc = 3 * sizeof(float);                                          //  RGB pixel size
   uint8       *Pmark;                                                           //  pixel undo markers
   int         Vmark;                                                            //  current Pmark value 0-255
   int         Wcolor;                                                           //  which color check button
   int         Bprox;                                                            //  background proximity range
   int         Fusehue;                                                          //  flag, use hue
}


//  menu function

void m_chromatic2(GtkWidget *, cchar *menu)                                      //  20.0
{
   using namespace chromatic2_names;
   
   int   chromatic2_dialog_event(zdialog* zd, cchar *event);
   void  chromatic2_mousefunc();
   
   cchar    *title = E2X("Chromatic Aberration");
   int      cc;

   F1_help_topic = "chromatic 2";

   EFchromatic2.menuname = menu;                                                 //  setup edit
   EFchromatic2.menufunc = m_chromatic2;
   EFchromatic2.funcname = "chromatic2";
   EFchromatic2.mousefunc = chromatic2_mousefunc;                                //  mouse function
   EFchromatic2.Farea = 2;                                                       //  select area OK
   if (! edit_setup(EFchromatic2)) return;

   Eww = E3pxm->ww;                                                              //  image dimensions
   Ehh = E3pxm->hh;
   
   cc = Eww * Ehh;                                                               //  allocate pixel markers
   Pmark = (uint8 *) zmalloc(cc);
   memset(Pmark,0,cc);

/***
       ______________________________________
      |       Chromatic Aberration           |
      |                                      |
      |  [x] Chromatic Color [ ### ]         |
      |  [_] Replacement Color [ ### ]       |
      |  [_] Background Color [ ### ]        |
      |  Color match level [ 70 ]            |
      |  Background proximity [ 10 ]         |
      |                                      |
      |       [apply] [undo] [done] [cancel] |
      |______________________________________|

***/

   zd = zdialog_new(title,Mwin,Bapply,Bundo,Bdone,Bcancel,null);
   CEF->zd = zd;
   
   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog");

   zdialog_add_widget(zd,"check","Ccheck","vb1",E2X("Chromatic Color"),"space=3");
   zdialog_add_widget(zd,"colorbutt","Crgb","vb2","0|0|0");

   zdialog_add_widget(zd,"check","Rcheck","vb1",E2X("Replacement Color"),"space=3");
   zdialog_add_widget(zd,"colorbutt","Rrgb","vb2","0|0|0");

   zdialog_add_widget(zd,"check","Bcheck","vb1",E2X("Background Color"),"space=3");
   zdialog_add_widget(zd,"colorbutt","Brgb","vb2","0|0|0");

   zdialog_add_widget(zd,"hbox","hb4","dialog");
   zdialog_add_widget(zd,"label","labmatch","hb4",E2X("Color match level"),"space=3");
   zdialog_add_widget(zd,"zspin","Cmatch","hb4","50|100|1|70","space=3");

   zdialog_add_widget(zd,"hbox","hb5","dialog");
   zdialog_add_widget(zd,"label","labp","hb5",E2X("Background Proximity"),"space=3");
   zdialog_add_widget(zd,"zspin","Bprox","hb5","1|100|1|10","space=3");
   
   zdialog_run(zd,chromatic2_dialog_event,"save");                               //  run dialog - parallel

   zdialog_stuff(zd,"Ccheck",1);
   zdialog_stuff(zd,"Rcheck",0);
   zdialog_stuff(zd,"Bcheck",0);

   Wcolor = 1;                                                                   //  next color = chromatic
   Cmatch = 70;                                                                  //  match level %
   Bprox = 10;                                                                   //  background proximity
   Vmark = 0;                                                                    //  no marks yet

   takeMouse(chromatic2_mousefunc,dragcursor);      
   return;
}


//  dialog event and completion function

int chromatic2_dialog_event(zdialog *zd, cchar *event)
{
   using namespace chromatic2_names;
   
   void chromatic2_mousefunc();
   void chromatic2_repair();
   
   int      ii;
   int      px, py;
   float    *pix1, *pix3;
   
   if (strmatch(event,"done")) zd->zstat = 3;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  from f_open()

   if (zd->zstat)
   {  
      if (zd->zstat == 1)                                                        //  [apply]
      {
         zd->zstat = 0;                                                          //  keep dialog active

         if (Vmark == 255) {                                                     //  check for max. marker value
            zmessageACK(Mwin,E2X("255 iterations, cannot continue"));
            return 1;
         }

         chromatic2_repair();                                                    //  do repair function
         return 1;
      }

      if (zd->zstat == 2)                                                        //  [undo]
      {
         zd->zstat = 0;                                                          //  keep dialog active

         if (Vmark < 1) return 1;                                                //  nothing to undo
         
         for (py = 0; py < Ehh; py++)
         for (px = 0; px < Eww; px++)
         {
            ii = py * Eww + px;
            if (Pmark[ii] == Vmark) {
               pix1 = PXMpix(E1pxm,px,py);
               pix3 = PXMpix(E3pxm,px,py);
               memcpy(pix3,pix1,pcc);
            }
         }
         
         Vmark -= 1;
         Fpaint2();
         return 1;
      }
      
      if (zd->zstat == 3) edit_done(0);                                          //  [done]
      else edit_cancel(0);                                                       //  [cancel]

      freeMouse();
      zfree(Pmark);
      return 1;
   }
   
   if (strmatch(event,"focus"))
      takeMouse(chromatic2_mousefunc,dragcursor);
   
   if (zstrstr("Ccheck Rcheck Bcheck",event)) {                                  //  mark which color to be selected
      zdialog_stuff(zd,"Ccheck",0);
      zdialog_stuff(zd,"Rcheck",0);
      zdialog_stuff(zd,"Bcheck",0);
      zdialog_stuff(zd,event,1);
      if (strmatch(event,"Ccheck")) Wcolor = 1;
      if (strmatch(event,"Rcheck")) Wcolor = 2;
      if (strmatch(event,"Bcheck")) Wcolor = 3;
   }
   
   if (strmatch(event,"Bprox"))                                                  //  background proximity limit
      zdialog_fetch(zd,"Bprox",Bprox);
   
   if (strmatch(event,"Cmatch"))
      zdialog_fetch(zd,"Cmatch",Cmatch);

   return 1;
}


//  mouse function

void chromatic2_mousefunc()
{
   using namespace chromatic2_names;
   
   float    *pix;
   char     text[20];

   if (! LMclick) return;
   LMclick = 0;
   if (! zd) return;
   
   pix = PXBpix(E1pxm,Mxclick,Myclick);                                          //  pick new color from image
   snprintf(text,20,"%.0f|%.0f|%.0f",pix[0],pix[1],pix[2]);
   
   if (Wcolor == 1) {                                                            //  chromatic color to fix
      Crgb[0] = pix[0];
      Crgb[1] = pix[1];
      Crgb[2] = pix[2];
      zdialog_stuff(zd,"Crgb",text);                                             //  update dialog color button
   }   

   if (Wcolor == 2) {                                                            //  replacement color
      Rrgb[0] = pix[0];
      Rrgb[1] = pix[1];
      Rrgb[2] = pix[2];
      zdialog_stuff(zd,"Rrgb",text);
   }   

   if (Wcolor == 3) {                                                            //  background color
      Brgb[0] = pix[0];
      Brgb[1] = pix[1];
      Brgb[2] = pix[2];
      zdialog_stuff(zd,"Brgb",text);
   }
   
   return;
}


//  repair the chromatic aberraton

void chromatic2_repair()
{
   using namespace chromatic2_names;

   void * chromatic2_repair_wthread(void *arg);
   
   float    rmin, rmax;
   char     text[20];
   cchar    *pp;
   
   zdialog_fetch(zd,"Crgb",text,20);                                             //  get chromatic color to fix
   pp = strField(text,'|',1);
   if (pp) Crgb[0] = atoi(pp);
   pp = strField(text,'|',2);
   if (pp) Crgb[1] = atoi(pp);
   pp = strField(text,'|',3);
   if (pp) Crgb[2] = atoi(pp);

   zdialog_fetch(zd,"Rrgb",text,20);                                             //  get replacement color
   pp = strField(text,'|',1);
   if (pp) Rrgb[0] = atoi(pp);
   pp = strField(text,'|',2);
   if (pp) Rrgb[1] = atoi(pp);
   pp = strField(text,'|',3);
   if (pp) Rrgb[2] = atoi(pp);

   zdialog_fetch(zd,"Brgb",text,20);                                             //  get background color
   pp = strField(text,'|',1);
   if (pp) Brgb[0] = atoi(pp);
   pp = strField(text,'|',2);
   if (pp) Brgb[1] = atoi(pp);
   pp = strField(text,'|',3);
   if (pp) Brgb[2] = atoi(pp);
   
   zdialog_fetch(zd,"Bprox",Bprox);                                              //  background proximity range
   zdialog_fetch(zd,"Cmatch",Cmatch);                                            //  color match level 
   
   RGBtoHSL(Rrgb[0],Rrgb[1],Rrgb[2],Rhsl[0],Rhsl[1],Rhsl[2]);                    //  replacement color in HSL units

   rmin = rmax = Rrgb[0];
   if (Rrgb[1] < rmin) rmin = Rrgb[1];                                           //  get replacement color RGB range
   if (Rrgb[2] < rmin) rmin = Rrgb[2]; 
   if (Rrgb[1] > rmax) rmax = Rrgb[1];
   if (Rrgb[2] > rmax) rmax = Rrgb[2];

   Fusehue = 0;                                                                  //  use replacement color hue
   if (rmax > 0 && (rmax-rmin) / rmax > 0.1) Fusehue = 1;                        //    only if distinct hue

   Vmark += 1;                                                                   //  next pixel marker value

   do_wthreads(chromatic2_repair_wthread,NWT);                                   //  do worker threads

   CEF->Fmods++;                                                                 //  image is modified
   CEF->Fsaved = 0;                                                              //  and not saved
   Fpaint2();                                                                    //  update window
   return;
}


void * chromatic2_repair_wthread(void *arg)                                      //  worker threads
{
   using namespace chromatic2_names;

   int      index = *((int *) arg);
   int      ii, dist;
   int      px, py, qx, qy;
   float    f1, f2;
   float    *pix1, *pix3;
   float    Prgb[3], Phsl[3];
   int      dx, dy, R, D;
   
   R = Bprox;
   
   for (py = R+index; py < Ehh-R; py += NWT)                                     //  loop all image pixels
   for (px = R; px < Eww-R; px++)                                                //  find pix1 matching chromatic color
   {
      ii = py * Eww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
      }

      pix3 = PXMpix(E1pxm,px,py);                                                //  input pixel
      f1 = PIXMATCH(pix3,Crgb);                                                  //  compare to chromatic color
      if (f1 < 0.01 * Cmatch) continue;                                          //  match level, 0.7 ... 1.0

      f1 = 3.333 * f1 - 2.333;                                                   //  part replacement color, 0.0 ... 1.0
      f1 = sqrtf(f1);                                                            //  use curve 
      f2 = 1.0 - f1;                                                             //  part original color, 1.0 ... 0.0
      
      D = 999;

      for (qy = py-R; qy <= py+R; qy++)                                          //  loop pixels within Bprox of pix3
      for (qx = px-R; qx <= px+R; qx++)
      {
         pix1 = PXMpix(E1pxm,qx,qy);
         if (PIXMATCH(pix1,Brgb) < 0.01 * Cmatch) continue;                      //  not a background pixel
         dx = qx - px;
         dy = qy - py;
         D = sqrtf(dx*dx + dy*dy);                                               //  image pixel distance to background
         if (D <= Bprox) goto break2;                                            //  exit both loops
      }  break2:

      if (D > Bprox) continue;                                                   //  pix3 too far from background
      
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      if (Fusehue) {                                                             //  use replacement color hue only
         RGBtoHSL(pix3[0],pix3[1],pix3[2],Phsl[0],Phsl[1],Phsl[2]);              //  convert to HSL
         Phsl[0] = Rhsl[0];                                                      //  replace hue only
         HSLtoRGB(Phsl[0],Phsl[1],Phsl[2],Prgb[0],Prgb[1],Prgb[2]);              //  back to RGB
         pix3[0] = f1 * Prgb[0] + f2 * pix3[0];                                  //  new = mix of new + old
         pix3[1] = f1 * Prgb[1] + f2 * pix3[1];
         pix3[2] = f1 * Prgb[2] + f2 * pix3[2];
      }
      
      else {
         pix3[0] = f1 * Rrgb[0] + f2 * pix3[0];                                  //  use replacement color RGB
         pix3[1] = f1 * Rrgb[1] + f2 * pix3[1];
         pix3[2] = f1 * Rrgb[2] + f2 * pix3[2];
      }

      Pmark[ii] = Vmark;                                                         //  mark undo level
   }
   
   pthread_exit(0);
}


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

   Vignette function

   1. Change the brightness from center to edge using a curve.
   2. Change the color from center to edge using a color and a curve.
      (the pixel varies between original RGB and selected color)
   3. Mouse click or drag on image sets a new vignette center.

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

void  vign_mousefunc();

editfunc    EFvignette;
uint8       vignette_RGB[3] = { 0, 0, 255 };
int         vignette_spc;
float       vign_cx, vign_cy;
float       vign_rad;


void m_vignette(GtkWidget *, cchar *)
{
   int      Vign_dialog_event(zdialog *zd, cchar *event);
   void     Vign_curvedit(int);
   void *   Vign_thread(void *);

   F1_help_topic = "vignette";

   cchar    *title = E2X("Vignette");

   EFvignette.menufunc = m_vignette;
   EFvignette.funcname = "vignette";
   EFvignette.Farea = 2;                                                         //  select area usable
   EFvignette.FprevReq = 1;                                                      //  use preview image
   EFvignette.threadfunc = Vign_thread;                                          //  thread function
   EFvignette.mousefunc = vign_mousefunc;                                        //  mouse function
   if (! edit_setup(EFvignette)) return;                                         //  setup edit

/***
          ___________________________________
         |  _______________________________  |
         | |                               | |
         | |                               | |
         | |    curve drawing area         | |
         | |                               | |
         | |_______________________________| |
         |  center                     edge  |
         |                                   |
         |  (o) Brightness  (o) Color [___]  |
         |  Curve File: [ Open ] [ Save ]    |
         |                                   |
         |                   [Done] [Cancel] |
         |___________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);
   EFvignette.zd = zd;

   zdialog_add_widget(zd,"frame","frame","dialog",0,"expand");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcenter","hb1",Bcenter,"space=4");
   zdialog_add_widget(zd,"label","space","hb1",0,"expand");
   zdialog_add_widget(zd,"label","labedge","hb1",Bedge,"space=5");

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","RBbrite","hb2",Bbrightness,"space=5");
   zdialog_add_widget(zd,"radio","RBcolor","hb2",Bcolor,"space=5");
   zdialog_add_widget(zd,"colorbutt","color","hb2","0|0|255");

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

   vignette_RGB[0] = vignette_RGB[1] = 0;                                        //  initial color = blue
   vignette_RGB[2] = 255;

   vign_cx = E3pxm->ww / 2;                                                      //  initial vignette center
   vign_cy = E3pxm->hh / 2;

   vign_rad = vign_cx * vign_cx + vign_cy * vign_cy;                             //  radius = distance to corners
   vign_rad = sqrtf(vign_rad);

   zdialog_stuff(zd,"RBbrite",1);                                                //  default curve = brightness

   GtkWidget *frame = zdialog_widget(zd,"frame");                                //  set up curve edit
   spldat *sd = splcurve_init(frame,Vign_curvedit);
   EFvignette.sd = sd;

   sd->Nspc = 2;                                                                 //  2 curves

   sd->vert[0] = 0;                                                              //  curve 0 = brightness curve
   sd->nap[0] = 2;
   sd->apx[0][0] = 0.01;
   sd->apy[0][0] = 0.5;
   sd->apx[0][1] = 0.99;
   sd->apy[0][1] = 0.5;
   splcurve_generate(sd,0);

   sd->vert[1] = 0;                                                              //  curve 1 = color curve
   sd->nap[1] = 2;
   sd->apx[1][0] = 0.01;
   sd->apy[1][0] = 0.01;
   sd->apx[1][1] = 0.99;
   sd->apy[1][1] = 0.01;
   splcurve_generate(sd,1);

   vignette_spc = 0;                                                             //  initial curve = brightness
   sd->fact[0] = 1;
   sd->fact[1] = 0; 

   zdialog_run(zd,Vign_dialog_event,"save");                                     //  run dialog - parallel

   takeMouse(vign_mousefunc,dragcursor);                                         //  connect mouse function
   return;
}


//  dialog event and completion callback function

int Vign_dialog_event(zdialog *zd, cchar *event)
{
   void     Vign_curvedit(int);

   spldat   *sd = EFvignette.sd;    
   int      ii;
   char     color[20];
   cchar    *pp;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      signal_thread();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         wait_thread_idle();                                                     //  insure thread done
         float R = 1.0 * E0pxm->ww / E3pxm->ww;
         vign_cx = R * vign_cx;                                                  //  scale geometries to full size
         vign_cy = R * vign_cy;
         vign_rad = R * vign_rad;
         edit_fullsize();                                                        //  get full size image
         signal_thread();
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

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

   if (strmatchN(event,"RB",2)) {                                                //  new choice of curve
      sd->fact[0] = sd->fact[1] = 0; 
      ii = strmatchV(event,"RBbrite","RBcolor",null);
      vignette_spc = ii = ii - 1;
      sd->fact[ii] = 1;                                                          //  active curve
      splcurve_generate(sd,ii);                                                  //  regenerate curve
      gtk_widget_queue_draw(sd->drawarea);                                       //  draw curve
      signal_thread();
   }

   if (strmatch(event,"blendwidth")) signal_thread();

   if (strmatch(event,"color")) {                                                //  change color
      zdialog_fetch(zd,"color",color,19);                                        //  get color from color wheel
      pp = strField(color,"|",1);
      if (pp) vignette_RGB[0] = atoi(pp);
      pp = strField(color,"|",2);
      if (pp) vignette_RGB[1] = atoi(pp);
      pp = strField(color,"|",3);
      if (pp) vignette_RGB[2] = atoi(pp);
      signal_thread();                                                           //  trigger update thread
   }

   if (strmatch(event,"load")) {                                                 //  load saved curve
      splcurve_load(sd);
      Vign_curvedit(0);
      signal_thread();
      return 0;
   }

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

   return 0;
}


//  get mouse position and set new center for vignette

void vign_mousefunc()                                                            //  mouse function
{
   if (! LMclick && ! Mdrag) return;
   LMclick = 0;

   vign_cx = Mxposn;                                                             //  new vignette center = mouse position
   vign_cy = Myposn;

   Mxdrag = Mydrag = 0;

   signal_thread();                                                              //  trigger image update
   return;
}


//  this function is called when the curve is edited

void Vign_curvedit(int)
{
   signal_thread();                                                              //  update image
   return;
}


//  thread function

void * Vign_thread(void *)
{
   void * Vign_wthread(void *arg);

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

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

      do_wthreads(Vign_wthread,NWT);                                             //  worker threads

      CEF->Fmods++;
      CEF->Fsaved = 0;

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

   return 0;                                                                     //  not executed, stop g++ warning
}


//  working thread

void * Vign_wthread(void *arg)
{
   float       *pix1, *pix3;
   int         index, ii, kk, px, py, dist = 0;
   float       cx, cy, rad, radx, rady, f1, f2, xval, yval;
   float       red1, green1, blue1, red3, green3, blue3, cmax;
   spldat      *sd = EFvignette.sd;    

   cx = vign_cx;                                                                 //  vignette center (mouse)
   cy = vign_cy;

   index = *((int *) arg);

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  loop all image pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      ii = py * E3pxm->ww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel is outside area
      }

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

      red1 = pix1[0];                                                            //  input RGB
      green1 = pix1[1];
      blue1 = pix1[2];

      radx = px - cx;                                                            //  distance from vignette center
      rady = py - cy;
      rad = sqrtf(radx*radx + rady*rady);                                        //  (px,py) distance from center

      xval = rad / vign_rad;                                                     //  scale 0 to 1.0
      kk = 999.0 * xval;                                                         //  scale 0 to 999
      if (kk > 999) kk = 999;                                                    //  beyond radius

      yval = sd->yval[0][kk];                                                    //  brightness curve y-value 0 to 1.0
      if (yval > 1.0) yval = 1.0;
      yval = 2.0 * yval;                                                         //  0 to 2.0

      red3 = yval * red1;                                                        //  adjust brightness
      green3 = yval * green1;
      blue3 = yval * blue1;

      yval = sd->yval[1][kk];                                                    //  color curve y-value 0 to 1.0
      if (yval > 1.0) yval = 1.0;
      f1 = yval;                                                                 //  0 to 1.0   new color
      f2 = 1.0 - f1;                                                             //  1.0 to 0   old color

      red3 = f1 * vignette_RGB[0] + f2 * red3;                                   //  mix input and vignette color
      green3 = f1 * vignette_RGB[1] + f2 * green3;
      blue3 = f1 * vignette_RGB[2] + f2 * blue3;

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         red3 = f1 * red3 + f2 * red1;
         green3 = f1 * green3 + f2 * green1;
         blue3 = f1 * blue3 + f2 * blue1;
      }

      cmax = red3;                                                               //  detect overflow
      if (green3 > cmax) cmax = green3;
      if (blue3 > cmax) cmax = blue3;

      if (cmax > 255.9) {                                                        //  stop overflow
         red3 = red3 * 255.9 / cmax;
         green3 = green3 * 255.9 / cmax;
         blue3 = blue3 * 255.9 / cmax;
      }

      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

   pthread_exit(0);
}


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

//  find and remove "dust" from an image (e.g. from a scanned dusty slide)
//  dust is defined as small dark areas surrounded by brighter areas
//  image 1   original with prior edits
//  image 3   accumulated dust removals that have been committed
//  image 9   committed dust removals + pending removal (work in process)

namespace dust_names
{
   editfunc    EFdust;

   int         spotspan;                                                         //  max. dustspot span, pixels
   int         spotspan2;                                                        //  spotspan **2
   float       brightness;                                                       //  brightness limit, 0 to 1 = white
   float       contrast;                                                         //  min. contrast, 0 to 1 = black/white
   int         *pixgroup;                                                        //  maps (px,py) to pixel group no.
   int         Fred;                                                             //  red pixels are on

   int         Nstack;

   struct spixstack {
      uint16      px, py;                                                        //  pixel group search stack
      uint16      direc;
   }  *pixstack;

   #define maxgroups 1000000
   int         Ngroups;
   int         groupcount[maxgroups];                                            //  count of pixels in each group
   float       groupbright[maxgroups];
   int         edgecount[maxgroups];                                             //  group edge pixel count
   float       edgebright[maxgroups];                                            //  group edge pixel brightness sum

   typedef struct {
      uint16      px1, py1, px2, py2;                                            //  pixel group extreme pixels
      int         span2;                                                         //  span from px1/py1 to px2/py2
   }  sgroupspan;

   sgroupspan    groupspan[maxgroups];
}


//  menu function

void m_remove_dust(GtkWidget *, const char *)
{
   using namespace dust_names;

   int    dust_dialog_event(zdialog *zd, cchar *event);
   void * dust_thread(void *);

   F1_help_topic = "remove dust";
   m_viewmode(0,"F");                                                            //  file view mode                     19.0

   EFdust.menufunc = m_remove_dust;
   EFdust.funcname = "remove_dust";
   EFdust.Farea = 2;                                                             //  select area usable
   EFdust.Frestart = 1;                                                          //  restart allowed
   EFdust.threadfunc = dust_thread;                                              //  thread function
   if (! edit_setup(EFdust)) return;                                             //  setup edit

   E9pxm = PXM_copy(E3pxm);                                                      //  image 9 = copy of image3
   Fred = 0;

   int cc = E1pxm->ww * E1pxm->hh * sizeof(int);
   pixgroup = (int *) zmalloc(cc);                                               //  maps pixels to assigned groups

   cc = E1pxm->ww * E1pxm->hh * sizeof(spixstack);
   pixstack = (spixstack *) zmalloc(cc);                                         //  pixel group search stack

/***
       ____________________________________________
      |                Remove Dust                 |
      |                                            |
      | spot size limit    =========[]===========  |
      | max. brightness    =============[]=======  |
      | min. contrast      ========[]============  |
      | [erase] [red] [undo last] [apply]          |
      |                                            |
      |                            [Done] [Cancel] |
      |____________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Remove Dust"),Mwin,Bdone,Bcancel,null);
   EFdust.zd = zd;

   zdialog_add_widget(zd,"hbox","hbssl","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labssl","hbssl",E2X("spot size limit"),"space=5");
   zdialog_add_widget(zd,"hscale","spotspan","hbssl","1|50|1|20","space=5|expand");
   zdialog_add_widget(zd,"hbox","hbmb","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labmb","hbmb",E2X("max. brightness"),"space=5");
   zdialog_add_widget(zd,"hscale","brightness","hbmb","1|999|1|700","space=5|expand");
   zdialog_add_widget(zd,"hbox","hbmc","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labmb","hbmc",E2X("min. contrast"),"space=5");
   zdialog_add_widget(zd,"hscale","contrast","hbmc","1|500|1|40","space=5|expand");
   zdialog_add_widget(zd,"hbox","hbbutts","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","erase","hbbutts",Berase,"space=5");
   zdialog_add_widget(zd,"button","red","hbbutts",Bred,"space=5");
   zdialog_add_widget(zd,"button","undo1","hbbutts",Bundolast,"space=5");
   zdialog_add_widget(zd,"button","apply","hbbutts",Bapply,"space=5");

   zdialog_resize(zd,300,0);
   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs

   zdialog_fetch(zd,"spotspan",spotspan);                                        //  max. dustspot span (pixels)
   spotspan2 = spotspan * spotspan;

   zdialog_fetch(zd,"brightness",brightness);                                    //  max. dustspot brightness
   brightness = 0.001 * brightness;                                              //  scale 0 to 1 = white

   zdialog_fetch(zd,"contrast",contrast);                                        //  min. dustspot contrast
   contrast = 0.001 * contrast;                                                  //  scale 0 to 1 = black/white

   zdialog_run(zd,dust_dialog_event,"save");                                     //  run dialog - parallel

   signal_thread();
   return;
}


//  dialog event and completion callback function

int dust_dialog_event(zdialog *zd, cchar *event)
{
   using namespace dust_names;

   void dust_erase();

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         wrapup_thread(8);                                                       //  thread finish, exit
         PXM_free(E3pxm);
         E3pxm = E9pxm;                                                          //  image 3 = image 9
         E9pxm = 0;
         edit_done(0);                                                           //  commit edit
      }
      else {                                                                     //  cancel
         wrapup_thread(8);                                                       //  thread finish, exit
         PXM_free(E9pxm);
         edit_cancel(0);                                                         //  discard edit
      }
      zfree(pixgroup);                                                           //  free memory
      zfree(pixstack);
      return 1;
   }

   if (zstrstr("spotspan brightness contrast red",event))
   {
      zdialog_fetch(zd,"spotspan",spotspan);                                     //  max. dustspot span (pixels)
      spotspan2 = spotspan * spotspan;

      zdialog_fetch(zd,"brightness",brightness);                                 //  max. dustspot brightness
      brightness = 0.001 * brightness;                                           //  scale 0 to 1 = white

      zdialog_fetch(zd,"contrast",contrast);                                     //  min. dustspot contrast
      contrast = 0.001 * contrast;                                               //  scale 0 to 1 = black/white

      signal_thread();                                                           //  do the work
   }

   if (strmatch(event,"erase")) dust_erase();
   if (strmatch(event,"blendwidth")) dust_erase();

   if (strmatch(event,"undo1")) {
      PXM_free(E3pxm);
      E3pxm = PXM_copy(E9pxm);
      Fred = 0;
      Fpaint2();
   }

   if (strmatch(event,"apply")) {                                                //  button
      if (Fred) dust_erase();
      PXM_free(E9pxm);                                                           //  image 9 = copy of image 3
      E9pxm = PXM_copy(E3pxm);
      CEF->Fmods++;
      CEF->Fsaved = 0;
   }

   return 1;
}


//  dust find thread function - find the dust particles and mark them

void * dust_thread(void *)
{
   using namespace dust_names;

   int         xspan, yspan, span2;
   int         group, cc, ii, kk, Nremoved;
   int         px, py, dx, dy, ppx, ppy, npx, npy;
   float       gbright, pbright, pcontrast;
   float       ff = 1.0 / 256.0;
   uint16      direc;
   float       *pix3;

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

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

      PXM_free(E3pxm);
      E3pxm = PXM_copy(E9pxm);

      paintlock(0);                                                              //  unblock window updates

      cc = E1pxm->ww * E1pxm->hh * sizeof(int);                                  //  clear group arrays
      memset(pixgroup,0,cc);
      cc = maxgroups * sizeof(int);
      memset(groupcount,0,cc);
      memset(edgecount,0,cc);
      cc = maxgroups * sizeof(float );
      memset(groupbright,0,cc);
      memset(edgebright,0,cc);
      cc = maxgroups * sizeof(sgroupspan);
      memset(groupspan,0,cc);

      group = 0;

      for (py = 0; py < E1pxm->hh; py++)                                         //  loop all pixels
      for (px = 0; px < E1pxm->ww; px++)
      {
         ii = py * E1pxm->ww + px;
         if (sa_stat == 3 && ! sa_pixmap[ii]) continue;                          //  not in active area
         if (pixgroup[ii]) continue;                                             //  already assigned to a group

         pix3 = PXMpix(E3pxm,px,py);                                             //  get pixel brightness
         gbright = ff * pixbright(pix3);                                         //  0 to 1.0 = white
         if (gbright > brightness) continue;                                     //  ignore bright pixel

         if (group == maxgroups-1) break;                                        //  too many groups, make no more

         pixgroup[ii] = ++group;                                                 //  assign next group
         groupcount[group] = 1;
         groupbright[group] = gbright;

         pixstack[0].px = px;                                                    //  put pixel into stack with
         pixstack[0].py = py;                                                    //    direction = ahead
         pixstack[0].direc = 0;
         Nstack = 1;

         while (Nstack)
         {
            kk = Nstack - 1;                                                     //  get last pixel in stack
            px = pixstack[kk].px;
            py = pixstack[kk].py;
            direc = pixstack[kk].direc;                                          //  next search direction

            if (direc == 'x') {
               Nstack--;                                                         //  none left
               continue;
            }

            if (Nstack > 1) {
               ii = Nstack - 2;                                                  //  get prior pixel in stack
               ppx = pixstack[ii].px;
               ppy = pixstack[ii].py;
            }
            else {
               ppx = px - 1;                                                     //  if only one, assume prior = left
               ppy = py;
            }

            dx = px - ppx;                                                       //  vector from prior to this pixel
            dy = py - ppy;

            switch (direc)
            {
               case 0:
                  npx = px + dx;
                  npy = py + dy;
                  pixstack[kk].direc = 1;
                  break;

               case 1:
                  npx = px + dy;
                  npy = py + dx;
                  pixstack[kk].direc = 3;
                  break;

               case 2:
                  npx = px - dx;                                                 //  back to prior pixel
                  npy = py - dy;                                                 //  (this path never taken)
                  zappcrash("stack search bug");
                  break;

               case 3:
                  npx = px - dy;
                  npy = py - dx;
                  pixstack[kk].direc = 4;
                  break;

               case 4:
                  npx = px - dx;
                  npy = py + dy;
                  pixstack[kk].direc = 5;
                  break;

               case 5:
                  npx = px - dy;
                  npy = py + dx;
                  pixstack[kk].direc = 6;
                  break;

               case 6:
                  npx = px + dx;
                  npy = py - dy;
                  pixstack[kk].direc = 7;
                  break;

               case 7:
                  npx = px + dy;
                  npy = py - dx;
                  pixstack[kk].direc = 'x';
                  break;

               default:
                  npx = npy = 0;
                  zappcrash("stack search bug");
            }

            if (npx < 0 || npx > E1pxm->ww-1) continue;                          //  pixel off the edge
            if (npy < 0 || npy > E1pxm->hh-1) continue;

            ii = npy * E1pxm->ww + npx;
            if (pixgroup[ii]) continue;                                          //  pixel already assigned
            if (sa_stat == 3 && ! sa_pixmap[ii]) continue;                       //  pixel outside area

            pix3 = PXMpix(E3pxm,npx,npy);                                        //  pixel brightness
            pbright = ff * pixbright(pix3);
            if (pbright > brightness) continue;                                  //  brighter than limit

            pixgroup[ii] = group;                                                //  assign pixel to group
            ++groupcount[group];                                                 //  count pixels in group
            groupbright[group] += pbright;                                       //  sum brightness for group

            kk = Nstack++;                                                       //  put pixel into stack
            pixstack[kk].px = npx;
            pixstack[kk].py = npy;
            pixstack[kk].direc = 0;                                              //  search direction
         }
      }

      Ngroups = group;                                                           //  group numbers are 1-Ngroups
      Nremoved = 0;

      for (py = 0; py < E1pxm->hh; py++)                                         //  loop all pixels
      for (px = 0; px < E1pxm->ww; px++)
      {
         ii = py * E1pxm->ww + px;
         group = pixgroup[ii];
         if (! group) continue;
         if (groupspan[group].px1 == 0) {                                        //  first pixel found in this group
            groupspan[group].px1 = px;                                           //  group px1/py1 = this pixel
            groupspan[group].py1 = py;
            continue;
         }
         xspan = groupspan[group].px1 - px;                                      //  span from group px1/py1 to this pixel
         yspan = groupspan[group].py1 - py;
         span2 = xspan * xspan + yspan * yspan;
         if (span2 > groupspan[group].span2) {
            groupspan[group].span2 = span2;                                      //  if greater, group px2/py2 = this pixel
            groupspan[group].px2 = px;
            groupspan[group].py2 = py;
         }
      }

      for (py = 0; py < E1pxm->hh; py++)                                         //  loop all pixels
      for (px = 0; px < E1pxm->ww; px++)
      {
         ii = py * E1pxm->ww + px;
         group = pixgroup[ii];
         if (! group) continue;
         if (groupspan[group].span2 > spotspan2) continue;
         xspan = groupspan[group].px2 - px;                                      //  span from this pixel to group px2/py2
         yspan = groupspan[group].py2 - py;
         span2 = xspan * xspan + yspan * yspan;
         if (span2 > groupspan[group].span2) {
            groupspan[group].span2 = span2;                                      //  if greater, group px1/py1 = this pixel
            groupspan[group].px1 = px;
            groupspan[group].py1 = py;
         }
      }

      for (py = 0; py < E1pxm->hh; py++)                                         //  loop all pixels
      for (px = 0; px < E1pxm->ww; px++)
      {
         ii = py * E1pxm->ww + px;                                               //  eliminate group if span > limit
         group = pixgroup[ii];
         if (! group) continue;
         if (! groupcount[group]) pixgroup[ii] = 0;
         else if (groupspan[group].span2 > spotspan2) {
            pixgroup[ii] = 0;
            groupcount[group] = 0;
            Nremoved++;
         }
      }

      for (py = 1; py < E1pxm->hh-1; py++)                                       //  loop all pixels except image edges
      for (px = 1; px < E1pxm->ww-1; px++)
      {
         ii = py * E1pxm->ww + px;
         group = pixgroup[ii];
         if (group) continue;                                                    //  find pixels bordering group pixels
         pix3 = PXMpix(E3pxm,px,py);
         pbright = ff * pixbright(pix3);

         group = pixgroup[ii-E1pxm->ww-1];
         if (group) {
            ++edgecount[group];                                                  //  accumulate pixel count and
            edgebright[group] += pbright;                                        //      bordering the groups
         }

         group = pixgroup[ii-E1pxm->ww];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii-E1pxm->ww+1];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii-1];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii+1];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii+E1pxm->ww-1];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii+E1pxm->ww];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii+E1pxm->ww+1];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }
      }

      for (group = 1; group <= Ngroups; group++)                                 //  compute group pixel and edge pixel
      {                                                                          //    mean brightness
         if (groupcount[group] && edgecount[group]) {
            edgebright[group] = edgebright[group] / edgecount[group];
            groupbright[group] = groupbright[group] / groupcount[group];
            pcontrast = edgebright[group] - groupbright[group];                  //  edge - group contrast
            if (pcontrast < contrast) {
               groupcount[group] = 0;
               Nremoved++;
            }
         }
      }

      for (py = 0; py < E1pxm->hh; py++)                                         //  loop all pixels
      for (px = 0; px < E1pxm->ww; px++)
      {
         ii = py * E1pxm->ww + px;                                               //  eliminate group if low contrast
         group = pixgroup[ii];
         if (! group) continue;
         if (! groupcount[group]) pixgroup[ii] = 0;
      }

      for (py = 0; py < E1pxm->hh; py++)                                         //  loop all pixels
      for (px = 0; px < E1pxm->ww; px++)
      {
         ii = py * E1pxm->ww + px;
         if (! pixgroup[ii]) continue;                                           //  not a dust pixel
         pix3 = PXMpix(E3pxm,px,py);                                             //  paint it red
         pix3[0] = 255;
         pix3[1] = pix3[2] = 0;
      }

      Fred = 1;

      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop g++ warning
}


//  erase the selected dust areas

void dust_erase()
{
   using namespace dust_names;

   int         cc, ii, px, py, inc;
   int         qx, qy, npx, npy;
   int         sx, sy, tx, ty;
   int         rad, dist, dist2, mindist2;
   float       slope, f1, f2;
   float       *pix1, *pix3;
   char        *pmap;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   Ffuncbusy = 1;
   PXM_free(E3pxm);                                                              //  not a thread
   E3pxm = PXM_copy(E9pxm);

   cc = E1pxm->ww * E1pxm->hh;                                                   //  allocate pixel done map
   pmap = (char *) zmalloc(cc);
   memset(pmap,0,cc);

   for (py = 0; py < E1pxm->hh; py++)                                            //  loop all pixels
   for (px = 0; px < E1pxm->ww; px++)
   {
      ii = py * E1pxm->ww + px;
      if (! pixgroup[ii]) continue;                                              //  not a dust pixel
      if (pmap[ii]) continue;                                                    //  skip pixels already done

      mindist2 = 999999;
      npx = npy = 0;

      for (rad = 1; rad < 10; rad++)                                             //  find nearest edge (10 pixel limit)
      {
         for (qx = px-rad; qx <= px+rad; qx++)                                   //  search within rad
         for (qy = py-rad; qy <= py+rad; qy++)
         {
            if (qx < 0 || qx >= E1pxm->ww) continue;                             //  off image edge
            if (qy < 0 || qy >= E1pxm->hh) continue;
            ii = qy * E1pxm->ww + qx;
            if (pixgroup[ii]) continue;                                          //  within dust area

            dist2 = (px-qx) * (px-qx) + (py-qy) * (py-qy);                       //  distance**2 to edge pixel
            if (dist2 < mindist2) {
               mindist2 = dist2;
               npx = qx;                                                         //  save nearest pixel found
               npy = qy;
            }
         }

         if (rad * rad >= mindist2) break;                                       //  can quit now
      }

      if (! npx && ! npy) continue;                                              //  should not happen

      qx = npx;                                                                  //  nearest edge pixel
      qy = npy;

      if (abs(qy - py) > abs(qx - px)) {                                         //  qx/qy = near edge from px/py
         slope = 1.0 * (qx - px) / (qy - py);
         if (qy > py) inc = 1;
         else inc = -1;
         for (sy = py; sy != qy+inc; sy += inc)                                  //  line from px/py to qx/qy
         {
            sx = px + slope * (sy - py);
            ii = sy * E1pxm->ww + sx;
            if (pmap[ii]) continue;
            pmap[ii] = 1;
            tx = qx + (qx - sx);                                                 //  tx/ty = parallel line from qx/qy
            ty = qy + (qy - sy);
            if (tx < 0) tx = 0;
            if (tx > E1pxm->ww-1) tx = E1pxm->ww-1;
            if (ty < 0) ty = 0;
            if (ty > E1pxm->hh-1) ty = E1pxm->hh-1;
            pix1 = PXMpix(E3pxm,tx,ty);                                          //  copy pixel from tx/ty to sx/sy
            pix3 = PXMpix(E3pxm,sx,sy);
            memcpy(pix3,pix1,pcc);
         }
      }

      else {
         slope = 1.0 * (qy - py) / (qx - px);
         if (qx > px) inc = 1;
         else inc = -1;
         for (sx = px; sx != qx+inc; sx += inc)
         {
            sy = py + slope * (sx - px);
            ii = sy * E1pxm->ww + sx;
            if (pmap[ii]) continue;
            pmap[ii] = 1;
            tx = qx + (qx - sx);
            ty = qy + (qy - sy);
            if (tx < 0) tx = 0;
            if (tx > E1pxm->ww-1) tx = E1pxm->ww-1;
            if (ty < 0) ty = 0;
            if (ty > E1pxm->hh-1) ty = E1pxm->hh-1;
            pix1 = PXMpix(E3pxm,tx,ty);
            pix3 = PXMpix(E3pxm,sx,sy);
            memcpy(pix3,pix1,pcc);
         }
      }
   }

   zfree(pmap);

   if (sa_stat == 3)                                                             //  area edge blending
   {
      for (ii = 0; ii < E1pxm->ww * E1pxm->hh; ii++)                             //  find pixels in select area
      {
         dist = sa_pixmap[ii];
         if (! dist || dist >= sa_blendwidth) continue;

         py = ii / E1pxm->ww;
         px = ii - py * E1pxm->ww;
         pix1 = PXMpix(E1pxm,px,py);                                             //  input pixel, unchanged image
         pix3 = PXMpix(E3pxm,px,py);                                             //  output pixel, changed image

         f2 = sa_blendfunc(dist);
         f1 = 1.0 - f2;

         pix3[0] = f1 * pix1[0] + f2 * pix3[0];                                  //  blend the pixels
         pix3[1] = f1 * pix1[1] + f2 * pix3[1];
         pix3[2] = f1 * pix1[2] + f2 * pix3[2];
      }
   }

   Fred = 0;
   Ffuncbusy = 0;
   Fpaint2();                                                                    //  update window
   return;
}


