Author Topic: Javascript: kappa-sigma clipping  (Read 23816 times)

Offline David Serrano

  • PTeam Member
  • PixInsight Guru
  • ****
  • Posts: 503
Javascript: kappa-sigma clipping
« on: 2007 August 24 16:55:49 »
[English below]

Buenas.

He estado dedicando unas horitas a implementar en javascript la suma de imágenes usando kappa-sigma clipping. El algoritmo está chupado, la parte difícil fue hacer que PixInsight hiciera algo ;) dada la actual escasez de documentación. Quería hacerle un interface pero lo he dejado para otro día porque me interesaba ver el código funcionando antes de que se me acabasen las vacas.

Va bastante lento: tarda 2 minutos en promediar dos imágenes de 640x420, a 1300 MHz. pero al menos he conseguido que vaya informando del progreso ;). Otra carencia importante es que la falta de interface obliga a los usuarios a editar el código antes de poder usarlo. Casi abajo de todo, en la línea que llama a combine ("Mixed" ...), hay que introducir los IDs de las imágenes que se quieren combinar (deben estar cargadas en PixInsight). Sólo lo he probado con dos.

El consumo de memoria es prácticamente nulo, y la profundidad en bits de la imagen resultante es el máximo de las profundidades de todas las imágenes que se combinan (en caso de que alguna sea distinta, por la razón que sea).

Tengo plan de licenciar este código bajo la GPL 3, aunque mi prudencia me lleva a llamar la atención de los Dioses por si hubiera algo que lo impidiese.

Por supuesto, cualquier clase de sugerencia, mejora u opinión es bienvenida!


Hi there,

I've spent some hours in implementing in Javascript the algorithm for combining images using kappa-sigma clipping. The algorithm itself is straightforward, the tough part was to make PixInsight do something ;) given the current lack of documentation. I wanted to make an interface for it but I left it for later since I was interested in seeing the code working before my holidays ended.

It's quite slow: it takes 2 minutes to average two 640x420 images, at 1300 MHz, but at least I've managed to convince the script to notify of its progress ;). Another important misfeature is that the lack of interface forces the users to edit the code before being able to use it. Near the bottom, at the line that calls combine ("Mixed" ...), you have to introduce the IDs of the images you want to merge (they must be loaded in PixInsight). I only tested the script with two images.

The memory consumption is virtually inexistent, and the bit depth of the resulting image is the maximum of the depths of all the images to be combined (in case some of them was different for any reason).

I plan to licence this code under the GPL 3, although I'm prudent enough to attract the attention of the Gods, just in case there was something preventing me to do it.

Of course, any kind of suggestions, improvements or opinions are welcome!

Code: [Select]
// Maybe some of these aren't needed...
#include <pjsr/Sizer.jsh>
#include <pjsr/FrameStyle.jsh>
#include <pjsr/TextAlign.jsh>
#include <pjsr/StdButton.jsh>
#include <pjsr/StdIcon.jsh>
#include <pjsr/ColorSpace.jsh>
#include <pjsr/UndoFlag.jsh>

function get_mean(set) {
   var tot = 0;
   for (i in set) {
      tot += set[i];
   }
   return tot / set.length;
}

function get_stddev(set) {
   // TODO: err if set.length < 2
   var mean = get_mean (set);
   var stddev = 0;
   for (i in set) {
      stddev += Math.pow ((set[i] - mean), 2);
   }
   stddev = Math.sqrt (stddev / set.length);
   return stddev;
}

function kappa_sigma_clipping(kappa, set) {
   var stddev;
   var mean;
   var low;
   var high;
   var result;
   while (1) {
      stddev = get_stddev(set);
      mean = get_mean(set);
      low  = mean - (kappa * stddev);
      high = mean + (kappa * stddev);
      result = new Array;  // begin with a new result set

      for (i in set) {
         if (set[i] >= low && set[i] <= high) {
           result.push (set[i]);
         }
      }
      // if lengths are equal, all elements are between boundaries: all done
      if (result.length == set.length) {
         break;
      }
      set = result;   // reset the work set
   }
   return result;
}


function combine (dstID, imgIDs) {
   var v = new Array;
   var img = new Array;
   var w = new Array;
   var h = new Array;
   var bps = new Array;
   for (i in imgIDs) {
      v[i] = new View (imgIDs[i]);
      img[i] = v[i].image;
      w[i] = img[i].width;
      h[i] = img[i].height;
      bps[i] = img[i].bitsPerSample;
   }
   bps = Math.max (bps);

   // TODO: check that all w[*] are the same. Ditto h[*]

   var dstimg = new Image (w[0], h[0], 3);
   dstimg.statusEnabled = true;
   dstimg.initializeStatus ("Averaging", w[0] * h[0])

   for (iy = 0; iy < h[0]; iy++) {
      for (ix = 0; ix < w[0]; ix++) {
         for (ic = 0; ic < 3; ic++) {
            var pixels = new Array;
            for (i in imgIDs) {
               pixels[i] = img[i].interpolate (ix, iy, ic);
            }
            dstimg.setSample (get_mean (kappa_sigma_clipping (2, pixels)), ix, iy, ic);
         }
      }
      gc();
      dstimg.advanceStatus (w[0]);
   }
   dstimg.completeStatus();
   
   var window = new ImageWindow (w[0], h[0], 3, bps, false, true, dstID);
   var view = window.mainView;
   view.beginProcess (UndoFlag_NoSwapFile); // do not generate any swap file
   view.image.apply (dstimg);
   view.endProcess();

   // Generated image windows must be explicitly shown
   window.show();
}

/*
 * Script entry point.
 */
function main()
{
   var startts = new Date;
   // Hide the console while our dialog is active.
   console.hide();

   //var dialog = new fooDialog();
   for ( ;; )
   {
      if ( 0 )   // ( !dialog.execute() )
         break;

      // Since this is a relatively slow process, show the console to provide
      // feedback to the user.
      console.show();

      // Allow users to abort execution of this script.
      console.abortEnabled = true;

      combine ("Mixed", Array ("Image01", "Image011"));

      // Quit after successful execution.
      break;
   }
   var endts = new Date;
   console.writeln ((endts.getTime() - startts.getTime())/1000, " s");
}

main();

--
 David Serrano

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Javascript: kappa-sigma clipping
« Reply #1 on: 2007 August 24 19:42:28 »
Here we go / Allá vamos:

Code: [Select]

// ### TODO: Documentation, credits, licensing.

#include <pjsr/Sizer.jsh>
#include <pjsr/FrameStyle.jsh>
#include <pjsr/TextAlign.jsh>
#include <pjsr/StdButton.jsh>
#include <pjsr/StdIcon.jsh>
#include <pjsr/ColorSpace.jsh>
#include <pjsr/SampleType.jsh>
#include <pjsr/UndoFlag.jsh>

// ### FIXME: Add #feature-id, #feature-info and (optionally) #feature-icon
//            preprocessor entries for this script to be featurable on the
//            Script menu.

// ### FIXME: Complete source code documentation.

function kappa_sigma_engine()
{
   this.kappa = 2.0;
   this.imgURLs = new Array;
   this.views = new Array;

   /*
    * Loads all selected images as new windows and populates the views array.
    */
   this.load = function ()
   {
      console.writeln ("<end><cbr><br>=== Step 1: Load Images ===");
      var N = this.imgURLs.length;
      this.views.length = 0;
      for (var i = 0; i < N; i++) {
         var w = new ImageWindow (this.imgURLs[i]);
         if (w.isNull)
            throw Error ("Unable to load image: " + this.imgURLs[i]);
         this.views.push (new View (w.mainView));
      }
   };

   /*
    * Performs an iterative kappa-sigma clipping of a pixel set.
    */
   function clip (set)
   {
      var stddev;
      var mean;
      var delta;
      var low;
      var high;
      var result;
      while (1) {
         stddev = Math.stddev (set);
         mean = Math.avg (set);
         delta = this.kappa*stddev;
         low  = mean - delta;
         high = mean + delta;
         result = new Array;  // begin with a new result set
         for (var i = 0, n = set.length; i < n; ++i)
            if (set[i] >= low && set[i] <= high)
               result.push (set[i]);
         // Need at least 3 items to calculate a meaningful standard deviation
         if ( result.length < 3 )
            break;
         // if lengths are equal, all elements are between boundaries: all done
         if (result.length == set.length)
            break;
         set = result;   // reset the work set
      }
      return set;
   }

   /*
    * Performs a kappa-sigma integration of the selected views.
    */
   this.combine = function ()
   {
      var N = this.views.length;
      if (N < 3)
         throw Error( "At least three images are required for kappa-sigma integration." );
      var img = new Array (N);
      var w = 0;
      var h = 0;
      var n = 0;
      var bps = 0;
      var real = false;
      for (var i = 0; i < N; i++) {
         if (this.views[i].isNull)
            throw Error ("Nonexistent view: " + this.imgURLs[i]);
         img[i] = this.views[i].image;
         if (i == 0) {
            w = img[i].width;
            h = img[i].height;
            n = img[i].numberOfChannels;
         } else {
            if ( img[i].width != w || img[i].height != h || img[i].numberOfChannels != n )
               throw Error ("Incongruent image dimension(s): " + this.views[i].fullId());
         }
         bps = Math.max (bps, img[i].bitsPerSample);
         real = real || img[i].sampleType != SampleType_Integer;
      }

      console.writeln ("<end><cbr><br>=== Step 2: Image Integration ===");

      var dstimg = new Image (w, h, n,
                              (n != 1) ? ColorSpace_RGB : ColorSpace_Gray,
                              bps, real ? SampleType_Real : SampleType_Integer);
      dstimg.statusEnabled = true;
      dstimg.initializeStatus (format("Integrating %u images", N), w*h);

      var pixels = new Array (N);
      for (var iy = 0; iy < h; iy++) {
         for (var ix = 0; ix < w; ix++) {
            for (var ic = 0; ic < n; ic++) {
               for (var i = 0; i < N; i++)
                  pixels[i] = img[i].sample (ix, iy, ic);
               dstimg.setSample (Math.avg (clip (pixels)), ix, iy, ic);
            }
         }
         gc();
         dstimg.advanceStatus (w);
      }

      var window = new ImageWindow (1, 1, n, bps, real,
                                    dstimg.colorSpace != ColorSpace_Gray,
                                    format("kappa_sigma_integration_of_%u", N));
      var view = window.mainView;
      view.beginProcess (UndoFlag_NoSwapFile); // do not generate any swap file
      view.image.transfer (dstimg);
      view.endProcess();
      window.show();
      window.zoomToFit( false ); // don't zoom in
   };

   /*
    * Closes all selected image windows and destroys the views array.
    */
   this.unload = function ()
   {
      console.writeln ("<end><cbr><br>=== Step 3: Unload Images ===");
      for (var i = 0; i < this.views.length; i++)
         if ( !this.views[i].isNull )
            this.views[i].window.close();
      this.views.length = 0;
      gc();
      console.writeln ("<end><cbr>done.");
   }
}

var engine = new kappa_sigma_engine();

function kappa_sigma_dialog()
{
   this.__base__ = Dialog;
   this.__base__();

   //

   this.helpLabel = new Label (this);
   this.helpLabel.frameStyle = FrameStyle_Box;
   this.helpLabel.margin = 4;
   this.helpLabel.wordWrapping = true;
   this.helpLabel.useRichText = true;
   this.helpLabel.text = "<b>KappaSigmaCombine</b> - A script to integrate a set of images by the iterative kappa-sigma rejection method.<br>" +
                         "### TODO: Write a description for this script.";

   //

   this.target_List = new TreeBox (this);
   this.target_List.setMinSize( 500, 200 );
   this.target_List.numberOfColumns = 1;
   this.target_List.headerVisible = false;

   for ( var i = 0; i < engine.imgURLs.length; ++i ) {
      var node = new TreeBoxNode( this.target_List );
      node.checkable = true;
      node.checked = true;
      node.setText( 0, engine.imgURLs[i] );
   }

   this.targetAdd_Button = new PushButton (this);
   this.targetAdd_Button.text = " Add ";
   this.targetAdd_Button.toolTip = "<p>Add images for integration.</p>";

   this.targetAdd_Button.onClick = function ()
   {
      var ofd = new OpenFileDialog;
      ofd.multipleSelections = true;
      ofd.caption = "Select Images for Integration";
      ofd.loadImageFilters();

      if ( ofd.execute() )
         for (var i = 0; i < ofd.fileNames.length; ++i) {
            var node = new TreeBoxNode (this.dialog.target_List);
            node.checkable = true;
            node.checked = true;
            node.setText (0, ofd.fileNames[i]);
            engine.imgURLs.push (ofd.fileNames[i]);
         }
   };

   this.targetClear_Button = new PushButton (this);
   this.targetClear_Button.text = " Clear ";
   this.targetClear_Button.toolTip = "<p>Clear the image list.</p>";

   this.targetClear_Button.onClick = function()
   {
      this.dialog.target_List.clear();
      engine.imgURLs.length = 0;
   };

   this.targetDisableAll_Button = new PushButton (this);
   this.targetDisableAll_Button.text = " Disable All ";
   this.targetDisableAll_Button.toolTip = "<p>Disable all images.</p>";

   this.targetDisableAll_Button.onClick = function()
   {
      for ( var i = 0; i < this.dialog.target_List.numberOfChildren; ++i )
         this.dialog.target_List.child(i).checked = false;
   };

   this.targetEnableAll_Button = new PushButton (this);
   this.targetEnableAll_Button.text = " Enable All ";
   this.targetEnableAll_Button.toolTip = "<p>Enable all images.</p>";

   this.targetEnableAll_Button.onClick = function()
   {
      for ( var i = 0; i < this.dialog.target_List.numberOfChildren; ++i )
         this.dialog.target_List.child(i).checked = true;
   };

   this.targetRemoveDisabled_Button = new PushButton( this );
   this.targetRemoveDisabled_Button.text = " Remove Disabled ";
   this.targetRemoveDisabled_Button.toolTip = "<p>Remove all disabled images.</p>";

   this.targetRemoveDisabled_Button.onClick = function()
   {
      engine.imgURLs.length = 0;
      for (var i = 0; i < this.dialog.target_List.numberOfChildren; ++i)
         if (this.dialog.target_List.child(i).checked)
            engine.imgURLs.push (this.dialog.target_List.child(i).text(0));
      for (var i = this.dialog.target_List.numberOfChildren; --i >= 0;)
         if (!this.dialog.target_List.child(i).checked)
            this.dialog.target_List.remove(i);
   };

   this.targetButtons_Sizer = new HorizontalSizer;
   this.targetButtons_Sizer.spacing = 4;
   this.targetButtons_Sizer.add (this.targetAdd_Button);
   this.targetButtons_Sizer.addStretch();
   this.targetButtons_Sizer.add (this.targetClear_Button);
   this.targetButtons_Sizer.addStretch();
   this.targetButtons_Sizer.add (this.targetDisableAll_Button);
   this.targetButtons_Sizer.add (this.targetEnableAll_Button);
   this.targetButtons_Sizer.add (this.targetRemoveDisabled_Button);

   this.target_GroupBox = new GroupBox (this);
   this.target_GroupBox.title = "Integrated Images";
   this.target_GroupBox.sizer = new VerticalSizer;
   this.target_GroupBox.sizer.margin = 4;
   this.target_GroupBox.sizer.spacing = 4;
   this.target_GroupBox.sizer.add (this.target_List, 100);
   this.target_GroupBox.sizer.add (this.targetButtons_Sizer);

   //

   this.ok_Button = new PushButton (this);
   this.ok_Button.text = " OK ";

   this.ok_Button.onClick = function()
   {
      this.dialog.ok();
   };

   this.cancel_Button = new PushButton (this);
   this.cancel_Button.text = " Cancel ";

   this.cancel_Button.onClick = function()
   {
      this.dialog.cancel();
   };

   this.buttons_Sizer = new HorizontalSizer;
   this.buttons_Sizer.spacing = 4;
   this.buttons_Sizer.addStretch();
   this.buttons_Sizer.add (this.ok_Button);
   this.buttons_Sizer.add (this.cancel_Button);

   //

   this.sizer = new VerticalSizer;
   this.sizer.margin = 6;
   this.sizer.spacing = 6;
   this.sizer.add (this.helpLabel);
   this.sizer.addSpacing( 4 );
   this.sizer.add (this.target_GroupBox, 100);
   this.sizer.add (this.buttons_Sizer);

   this.windowTitle = "KappaSigmaCombine Script";
   this.adjustToContents();
}

kappa_sigma_dialog.prototype = new Dialog;

/*
 * Script entry point.
 */
function main()
{
   // Starting time
   var startts = new Date;

   // Hide the console while our dialog is active.
   console.hide();

   var dialog = new kappa_sigma_dialog();

   for ( ;; ) {
      if (!dialog.execute())
         break;

      // Since this is a relatively slow process, show the console to provide
      // feedback to the user.
      console.show();

      // Allow users to abort execution of this script.
      console.abortEnabled = true;

      // Perform.
      engine.load();
      engine.combine();
      engine.unload();

      // Quit after successful execution.
      break;
   }

   var endts = new Date;
   console.writeln (format( "<end><cbr><br>%.2f s",
                            (endts.getTime() - startts.getTime())/1000));
}

main();


Una iniciativa excelente, David  :D

Lo he probado con 15 imágenes en escala de grises de 500x600 píxeles y ha tardado unos 38 segundos en un dual core. No está mal. El resultado es perfecto.

Una mejora evidente sería almacenar las imágenes en archivos raw temporales para poder hacer la integración mediante lectura incremental por filas de píxeles, lo cual evitaría tener que abrir todas las imágenes simultáneamente. Pero esto es bastante más complejo.

Por supuesto, puedes licenciar esto como quieras; GPL 3 me parece perfecto. Cualquier cosa desarrollada sobre la plataforma PixInsight se puede licenciar, publicar, regalar o vender como el autor decida. No hay límites en este sentido.

=========

An excellent initiative, David  :D

I have tested the script with 15 500x600 grayscale images and it has taken 38 seconds on a dual core. That's not bad. The result is perfect.

An evident improvement would be to store all images as temporary raw files to perform the integration by incremental reading of individual pixel rows, which would save as from having to open all images simultaneously. This is way more complex, though.

Of course, you can license this code as you want; GPL 3 seems ok to me. Anything developed on the PixInsight platform can be licensed, published, gifted or sold just as the author decides. There are no limits in this sense.
Juan Conejero
PixInsight Development Team
http://pixinsight.com/

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Javascript: kappa-sigma clipping
« Reply #2 on: 2007 August 24 20:34:08 »
A bug fix and a (very slight) speed improvement / Un error corregido y una (muy ligera) mejora en velocidad:

Code: [Select]
  function clip (set)
   {
      var stddev;
      var mean;
      var delta;
      var low;
      var high;
      var result;
      while (1) {
         stddev = Math.stddev (set);
         mean = Math.avg (set);
         delta = this.kappa*stddev;
         low  = mean - delta;
         high = mean + delta;
         result = new Array;  // begin with a new result set
         for (var i = 0, n = set.length; i < n; ++i) {
            var v = set[i];
            if (v >= low && v <= high)
               result.push (v);
         }
         // Need at least 3 items to calculate a meaningful standard deviation
         if ( result.length < 3 )
            return set;
         // if lengths are equal, all elements are between boundaries: all done
         if (result.length == set.length)
            return result;
         set = result;   // reset the work set
      }
   }


Replace the old code by this one in kappa_sigma_engine.

This routine is the script's main bottleneck. I've conducted an experiment sorting the pixel stack (the set array above) and applying two binary searches to locate the clipping array indexes, but there is a substantial performance degradation. Perhaps not for a very large set of images, but I've tested with 30 and the linear code above still runs quite faster. Scripting oddities...

==============

Reemplaza el código anterior en kappa_sigma_engine por éste.

Esta rutina es el principal cuello de botella del script. He hecho un experimento ordenando el conjunto de píxeles (el array set en el código) y aplicando dos búsquedas binarias para localizar los índices del array para los puntos de corte, pero hay una degradación de prestaciones sustancial. Puede que no para un conjunto muy grande de imágenes, pero he probado con 30 y el código lineal de arriba todavía se ejecuta bastante más rápido. Cosas curiosas con los lenguajes de script...
Juan Conejero
PixInsight Development Team
http://pixinsight.com/

Offline Carlos Milovic

  • PTeam Member
  • PixInsight Jedi Master
  • ******
  • Posts: 2172
  • Join the dark side... we have cookies
    • http://www.astrophoto.cl
Javascript: kappa-sigma clipping
« Reply #3 on: 2007 August 24 20:43:38 »
Como Juan dijo, excelente iniciativa :) A ver si te animas para escribirlo como un modulo de procesamiento ;) es bastante sencillo, y aqui nos tienes para ayudarte. Estoy seguro de que el rendimiento sera mucho mayor.
Regards,

Carlos Milovic F.
--------------------------------
PixInsight Project Developer
http://www.pixinsight.com

Offline David Serrano

  • PTeam Member
  • PixInsight Guru
  • ****
  • Posts: 503
Javascript: kappa-sigma clipping
« Reply #4 on: 2007 August 25 03:56:06 »
Quote from: "Carlos Milovic"
A ver si te animas para escribirlo como un modulo de procesamiento ;) es bastante sencillo, y aqui nos tienes para ayudarte.


Estaba leyendo el foro ahora en modo de sólo lectura porque empiezan los entrenos de F1 y no quería liarme a escribir nada. Pero hmmm... este párrafo... hmmm... :twisted:


Quote from: "Carlos Milovic"
Estoy seguro de que el rendimiento sera mucho mayor.


De eso no me cabe ninguna duda :). Primera pregunta, un poco tonta pero bueno: se puede hacer en C, ¿verdad? No hace falta que sea C++...

Juan, luego miro tus mejoras. Gracias!
--
 David Serrano

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Javascript: kappa-sigma clipping
« Reply #5 on: 2007 August 25 04:09:08 »
Quote from: "David Serrano"
se puede hacer en C, ¿verdad? No hace falta que sea C++...


Nope. C++ only. Bad luck  8)

No, en serio, PCL es C++ y los módulos hay que escribirlos en C++, pero no te preocupes, es bastante más fácil de lo que parece, y además tienes enchufe con los dioses  :lol:

Me voy a ver los F1. Alooooooooonso!
Juan Conejero
PixInsight Development Team
http://pixinsight.com/

Offline David Serrano

  • PTeam Member
  • PixInsight Guru
  • ****
  • Posts: 503
Javascript: kappa-sigma clipping
« Reply #6 on: 2007 August 25 06:29:36 »
Quote from: "Juan Conejero"
Code: [Select]
stddev = Math.stddev (set);
mean = Math.avg (set);


Sabía que esto tenía que existir por algún sitio... ahora es cuando me devuelves aquello de reinventar la rueda ;)

Muchas gracias por la revisión completa del script: orientación a objetos, inclusión de constantes en lugar de números mágicos, interface... y por supuesto también por el testing. Ahora mismo estoy alineando unas cositas ajenas y luego probaré a sumar, a ver qué tal.

Quote from: "Juan Conejero"
PCL es C++ y los módulos hay que escribirlos en C++


Sux0r. Como has visto en el código, mi cerebro no está tan orientado a objetos como el tuyo y la verdad es que me cuesta bastante. Bueno, ya que existe el fuente de varios módulos (algunos aparentemente sencillitos como ImageIdentifier y ReadoutOptions) probaré a echarles un ojo. Pero no prometo nada eh? :^P.

Y en una nueva versión de Pixi queremos un motorcito de Perl!!
--
 David Serrano

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Javascript: kappa-sigma clipping
« Reply #7 on: 2007 August 25 09:38:04 »
Quote
Como has visto en el código, mi cerebro no está tan orientado a objetos como el tuyo


Too much shell scripting, where false != 0 and true == 0 are both true. Hmmm... :lol:

Quote
Y en una nueva versión de Pixi queremos un motorcito de Perl!!


Bueno, podría ser. Pero espera a ver JavaScript  2 (= ECMA Script 4): una auténtica barbaridad. Se supone que habrá un motor de Mozilla (una evolución de SpiderMonkey) el año que viene. Tiene orientación a objetos basada en clases (como C++), tipos de datos numéricos optimizados, etc. Debería correr mucho más rápido que el JS 1.x actual.

También podríamos integrar Python. A mucha gente le gusta (no es mi caso)...
Juan Conejero
PixInsight Development Team
http://pixinsight.com/

Offline David Serrano

  • PTeam Member
  • PixInsight Guru
  • ****
  • Posts: 503
Javascript: kappa-sigma clipping
« Reply #8 on: 2007 August 25 10:46:12 »
Quote from: "Juan Conejero"
Too much shell scripting, where false != 0 and true == 0 are both true. Hmmm... :lol:


No, lo mío es Perl :^P.


Quote from: "Juan Conejero"
También podríamos integrar Python. A mucha gente le gusta (no es mi caso)...


Yo pienso que cada lenguaje tiene sus cosas. Al principio me pareció abominable pero ya me ha despertado el gusanillo...

Volviendo al tema principal, acabo de probar la versión ampliada por ti. Me ha dejado prácticamente todos los píxels calientes en su sitio  :roll:, lo cual probablemente es culpa de un valor kappa demasiado alto. Mi idea del interface era mostrar una lista de imágenes cargadas y previews existentes actualmente, y por supuesto una textbox para el valor kappa. De esta forma, podemos jugar con kappa rápidamente aplicando a un preview (el mismo preview en todas las imágenes) y cuando el resultado nos guste, dar caña a las imágenes enteras.

Si te apetece hacerlo, adelante :^P pero personalmente preferiría aprender a pescar ;). Veo un objeto ViewList con un método getAll muy jugoso, pero me parece que no permite seleccionar varias imágenes a la vez. Así que pienso en un ScrollBox, al que se le puede meter el resultado de ViewList.getAll con un bucle. ¿Estaría bien así? ¿O hay una forma mejor de hacerlo?
--
 David Serrano

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Javascript: kappa-sigma clipping
« Reply #9 on: 2007 August 25 10:51:15 »
Tus deducciones son correctas, excepto que en vez de ScrollBox debes utilizar un TreeBox. Básicamente la interfaz que quieres hacer es muy parecida a la que hay ahora. Fíjate que TreeBox se puede utilizar como una simple lista de elementos (no definiendo más que un nivel en el árbol). Entonces lo que tendrías que hacer es, en vez del botón Add que hay ahora para cargar archivos, poner uno que cargara todas las vistas disponibles. Es relativamente sencillo.

Edito: además debes poner un NumericControl para kappa. Mira cualquiera de los scripts de ejemplo que usan NumericControl; es muy fácil de usar (y no olvides hacer #include <pjsr/NumericControl.jsh>).

Pesca, pesca :)
Juan Conejero
PixInsight Development Team
http://pixinsight.com/

Offline David Serrano

  • PTeam Member
  • PixInsight Guru
  • ****
  • Posts: 503
Javascript: kappa-sigma clipping
« Reply #10 on: 2007 August 27 00:10:11 »
Quote from: "Juan Conejero"
Edito: además debes poner un NumericControl para kappa.


En esto te he ganado :^P. Miré primero un SpinBox pero no soporta decimales (que yo sepa) así que luego en otro script me topé con el NumericControl y no le di más vueltas. Una pregunta: ¿se puede ajustar el intervalo entre cada dos posiciones? Ahora mismo es de 0.2 pero me gustaría ponerlo de 0.1 o incluso de 0.05.

Siguiendo el espíritu de "Release early, release often", he aquí la versión actual. He tenido que cambiar "function clip() { }" por "this.clip = function() { }" porque de lo contrario, el valor de kappa se perdía por el camino al llamar a esta función. Le he puesto un contadorcito de píxels eliminados y ahora hay que cargar las imágenes antes de ejecutar el script. Funciona sobre previews.

Observo que se ejecuta sobre un sólo procesador aunque el sistema tenga más de uno, lo cual me resulta interesante (pero no sorprendente). ¿Podrá el usuario programar sobre varios procesadores? ¿Es "Haz un módulo" la respuesta? ;)

No he quitado algunos botones que ya no sirven para nada porque he visto ciertos comportamientos extraños que quiero reproducir ;). También he dejado por ahí algo de código muerto por si las moscas.

English short summary
This is the current version. It features a counter of removed pixels and now the user has to load the images in the application prior to run the script; this way you can define some previews and fine tune the kappa value quickly. There are still some unneeded interface elements and code left. Needless to say, they will be removed in a future release.

Code: [Select]
/*
    KSIntegration.js v0.3 - Image integration with kappa-sigma clipping.
    Copyright (C) 2007  David Serrano, Juan Conejero

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

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

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

// TODO: usage and source docs.
// TODO: make the coding style homogeneous.
// TODO: remove dead code and useless interface elements.
// TODO: optionally (de)select all images of the same dimensions and bit
//   depth when one of them is (de)selected.
// TODO: parallelize?

#include <pjsr/Sizer.jsh>
#include <pjsr/FrameStyle.jsh>
#include <pjsr/NumericControl.jsh>
#include <pjsr/TextAlign.jsh>
#include <pjsr/StdButton.jsh>
#include <pjsr/StdIcon.jsh>
#include <pjsr/ColorSpace.jsh>
#include <pjsr/SampleType.jsh>
#include <pjsr/UndoFlag.jsh>

#feature-id KSIntegration
#feature-info Implementation of image combination using kappa-sigma \
  method for rejecting hotpixels. Needs that the images to be combined \
  are previously loaded in the application. They must have the same size \
  and bit depth. Generates a new image with the result of the combination.
// #feature-icon KSIntegration.xpm

function kappa_sigma_engine()
{
   this.kappa = 2.0;
   this.imgURLs = new Array;
   this.views = new Array;

   /*
    * Loads all selected images as new windows and populates the views array.
    */
   this.load = function ()
   {
      console.writeln ("<end><cbr><br>=== Step 1: Load Images ===");
      var N = this.imgURLs.length;
      this.views.length = 0;
      for (var i = 0; i < N; i++) {
         this.views.push (new View (this.imgURLs[i]));
      }
   };

   /*
    * Performs an iterative kappa-sigma clipping of a pixel set.
    */
   this.clip = function (set)
   {
      var stddev;
      var mean;
      var delta;
      var low;
      var high;
      var result;
      while (1) {
         stddev = Math.stddev (set);
         mean = Math.avg (set);
         delta = this.kappa*stddev;
         low  = mean - delta;
         high = mean + delta;
         result = new Array;  // begin with a new result set
         for (var i = 0, n = set.length; i < n; ++i) {
            var v = set[i];
            if (v >= low && v <= high)
               result.push (v);
         }
         // Need at least 3 items to calculate a meaningful standard deviation
         if ( result.length < 3 )
            return set;
         // if lengths are equal, all elements are between boundaries: all done
         if (result.length == set.length)
            return result;
         set = result;   // reset the work set
      }
   }

   /*
    * Performs a kappa-sigma integration of the selected views.
    */
   this.combine = function ()
   {
      var N = this.views.length;
      if (N < 3)
         throw Error( "At least three images are required for kappa-sigma integration." );
      var img = new Array (N);
      var w = 0;
      var h = 0;
      var n = 0;
      var bps = 0;
      var real = false;
      for (var i = 0; i < N; i++) {
         if (this.views[i].isNull)
            throw Error ("Nonexistent view: " + this.views[i].fullId);
         img[i] = this.views[i].image;
         if (i == 0) {
            w = img[i].width;
            h = img[i].height;
            n = img[i].numberOfChannels;
         } else {
            if ( img[i].width != w || img[i].height != h || img[i].numberOfChannels != n )
               throw Error ("Incongruent image dimension(s): " + this.views[i].fullId);
         }
         bps = Math.max (bps, img[i].bitsPerSample);
         real = real || img[i].sampleType != SampleType_Integer;
      }

      console.writeln ("<end><cbr><br>=== Step 2: Image Integration ===");

      var dstimg = new Image (w, h, n,
                              (n != 1) ? ColorSpace_RGB : ColorSpace_Gray,
                              bps, real ? SampleType_Real : SampleType_Integer);
      dstimg.statusEnabled = true;
      dstimg.initializeStatus (format("Integrating %u images", N), w*h);

      var clipped;
      var nclipped = 0;
      var pixels = new Array (N);
      for (var iy = 0; iy < h; iy++) {
         for (var ix = 0; ix < w; ix++) {
            for (var ic = 0; ic < n; ic++) {
               for (var i = 0; i < N; i++) {
                  pixels[i] = img[i].sample (ix, iy, ic);
               }
               clipped = this.clip (pixels);
               dstimg.setSample (Math.avg (clipped), ix, iy, ic);
               if (clipped.length < N) {
                  nclipped += N - clipped.length;
               }
            }
         }
         gc();
         dstimg.advanceStatus (w);
      }

      console.writeln ("Clipped ", nclipped, " pixels from a total of ",
         (h*w*n*N)," (",(100*nclipped/(h*w*n*N)),"%)."); // FIXME
      var window = new ImageWindow (1, 1, n, bps, real,
                                    dstimg.colorSpace != ColorSpace_Gray,
                                    format("kappa_sigma_%d", 100*this.kappa));
      var view = window.mainView;
      view.beginProcess (UndoFlag_NoSwapFile); // do not generate any swap file
      view.image.transfer (dstimg);
      view.endProcess();
      window.show();
      window.zoomToFit( false ); // don't zoom in
   };

   /*
    * Closes all selected image windows and destroys the views array.
    */
   this.unload = function ()
   {
      // don't want our precious images closed, so exit without doing nothing
      gc(); return;

      console.writeln ("<end><cbr><br>=== Step 3: Unload Images ===");
      for (var i = 0; i < this.views.length; i++)
         if ( !this.views[i].isNull )
            this.views[i].window.close();
      this.views.length = 0;
      gc();
      console.writeln ("<end><cbr>done.");
   }
}

var engine = new kappa_sigma_engine();

function kappa_sigma_dialog()
{
   this.__base__ = Dialog;
   this.__base__();
   var emWidth = this.font.width( 'M' );

   //

   this.helpLabel = new Label (this);
   this.helpLabel.frameStyle = FrameStyle_Box;
   this.helpLabel.margin = 4;
   this.helpLabel.wordWrapping = true;
   this.helpLabel.useRichText = true;
   this.helpLabel.text = "<b>KSIntegration v0.3</b> - A script to integrate a set of images by the iterative kappa-sigma rejection method.<br>" +
      "Select the views to be combined and choose the kappa " +
      "value desired. Then click 'Ok' and wait for a new image to be created.";

   //

   this.target_List = new TreeBox (this);
   this.target_List.setMinSize( 500, 200 );
   this.target_List.font = new Font( "monospace", 10 ); // best to show tabulated data
   this.target_List.numberOfColumns = 3;
   this.target_List.headerVisible = true;
   this.target_List.headerSorting = true;
   this.target_List.setHeaderText( 0, "Views" );
   this.target_List.setHeaderText( 1, "Dimensions" );
   this.target_List.setHeaderText( 2, "Format" );
   this.target_List.setHeaderAlignment( 0, Align_Left );
   this.target_List.setHeaderAlignment( 1, Align_Left );
   this.target_List.setHeaderAlignment( 2, Align_Left );

   for ( var i = 0; i < engine.imgURLs.length; ++i ) {
      var node = new TreeBoxNode( this.target_List );
      node.checkable = true;
      node.checked = true;
      node.setText( 0, engine.imgURLs[i] );
   }

   // Node creation helper
   function addViewNode( parent, view )
   {
      var node = new TreeBoxNode( parent );
      node.checkable = true;
      node.checked = false;

      node.setText( 0, view.fullId );

      var image = view.image;
      node.setText( 1, format( "%5d x %5d x %d", image.width, image.height, image.numberOfChannels ) );

      if ( view.isMainView ) // don't show redundant info
      {
         var window = view.window;
         node.setText( 2, format( "%2d-bit %s", window.bitsPerSample, window.isFloatSample ? "floating point" : "integer" ) );
      }

      return node;
   }

   // Build the view tree structure
   var windows = ImageWindow.windows;
   for ( var i = 0; i < windows.length; ++i )
   {
      var node = addViewNode( this.target_List, windows[i].mainView );
      node.expanded = false; // or true to initially expand all preview lists

      var previews = windows[i].previews;
      for ( var j = 0; j < previews.length; ++j )
         addViewNode( this.target_List, previews[j] );
   }

   // Ensure that all columns are initially visible
   this.target_List.adjustColumnWidthToContents( 0 );
   this.target_List.adjustColumnWidthToContents( 1 );
   this.target_List.adjustColumnWidthToContents( 2 );
   this.target_List.sort();

   //

   this.targetAdd_Button = new PushButton (this);
   this.targetAdd_Button.text = " Add ";
   this.targetAdd_Button.toolTip = "<p>Add images for integration.</p>";

   this.targetAdd_Button.onClick = function ()
   {
      var ofd = new OpenFileDialog;
      ofd.multipleSelections = true;
      ofd.caption = "Select Images for Integration";
      ofd.loadImageFilters();

      if ( ofd.execute() )
         for (var i = 0; i < ofd.fileNames.length; ++i) {
            var node = new TreeBoxNode (this.dialog.target_List);
            node.checkable = true;
            node.checked = true;
            node.setText (0, ofd.fileNames[i]);
            engine.imgURLs.push (ofd.fileNames[i]);
         }
   };

   this.targetClear_Button = new PushButton (this);
   this.targetClear_Button.text = " Clear ";
   this.targetClear_Button.toolTip = "<p>Clear the image list.</p>";

   this.targetClear_Button.onClick = function()
   {
      this.dialog.target_List.clear();
      engine.imgURLs.length = 0;
   };

   this.targetDisableAll_Button = new PushButton (this);
   this.targetDisableAll_Button.text = " Disable All ";
   this.targetDisableAll_Button.toolTip = "<p>Disable all images.</p>";

   this.targetDisableAll_Button.onClick = function()
   {
      for ( var i = 0; i < this.dialog.target_List.numberOfChildren; ++i )
         this.dialog.target_List.child(i).checked = false;
   };

   this.targetEnableAll_Button = new PushButton (this);
   this.targetEnableAll_Button.text = " Enable All ";
   this.targetEnableAll_Button.toolTip = "<p>Enable all images.</p>";

   this.targetEnableAll_Button.onClick = function()
   {
      for ( var i = 0; i < this.dialog.target_List.numberOfChildren; ++i )
         this.dialog.target_List.child(i).checked = true;
   };

   this.targetRemoveDisabled_Button = new PushButton( this );
   this.targetRemoveDisabled_Button.text = " Remove Disabled ";
   this.targetRemoveDisabled_Button.toolTip = "<p>Remove all disabled images.</p>";

   this.targetRemoveDisabled_Button.onClick = function()
   {
      engine.imgURLs.length = 0;
      for (var i = 0; i < this.dialog.target_List.numberOfChildren; ++i)
         if (this.dialog.target_List.child(i).checked)
            engine.imgURLs.push (this.dialog.target_List.child(i).text(0));
      for (var i = this.dialog.target_List.numberOfChildren; --i >= 0;)
         if (!this.dialog.target_List.child(i).checked)
            this.dialog.target_List.remove(i);
   };

   this.targetButtons_Sizer = new HorizontalSizer;
   this.targetButtons_Sizer.spacing = 4;
   this.targetButtons_Sizer.add (this.targetAdd_Button);
   this.targetButtons_Sizer.addStretch();
   this.targetButtons_Sizer.add (this.targetClear_Button);
   this.targetButtons_Sizer.addStretch();
   this.targetButtons_Sizer.add (this.targetDisableAll_Button);
   this.targetButtons_Sizer.add (this.targetEnableAll_Button);
   this.targetButtons_Sizer.add (this.targetRemoveDisabled_Button);

   this.target_GroupBox = new GroupBox (this);
   this.target_GroupBox.title = "Integrated Images";
   this.target_GroupBox.sizer = new VerticalSizer;
   this.target_GroupBox.sizer.margin = 4;
   this.target_GroupBox.sizer.spacing = 4;
   this.target_GroupBox.sizer.add (this.target_List, 100);
   this.target_GroupBox.sizer.add (this.targetButtons_Sizer);

   //

   this.ok_Button = new PushButton (this);
   this.ok_Button.text = " OK ";

   this.ok_Button.onClick = function()
   {
      // couldn't achieve this using this.dialog.target_List.selectedNodes, its length is always 1
      for (n = 0; n < this.dialog.target_List.numberOfChildren; n++) {
         if (this.dialog.target_List.child(n).checked) {
            engine.imgURLs.push (this.dialog.target_List.child(n).text(0));
         }
      }
      this.dialog.ok();
   };

   this.cancel_Button = new PushButton (this);
   this.cancel_Button.text = " Cancel ";

   this.cancel_Button.onClick = function()
   {
      this.dialog.cancel();
   };

   this.buttons_Sizer = new HorizontalSizer;
   this.buttons_Sizer.spacing = 4;
   this.buttons_Sizer.addStretch();
   this.buttons_Sizer.add (this.ok_Button);
   this.buttons_Sizer.add (this.cancel_Button);

   //

   this.kappa_NC = new NumericControl( this );
   //this.kappa_NC.slider.tickInterval = 0.1;
   //this.kappa_NC.stepSize = 0.1;
   this.kappa_NC.label.text = "Kappa value:";
   this.kappa_NC.setRange (0.1, 10);
   this.kappa_NC.setPrecision (2);
   this.kappa_NC.setValue (engine.kappa);
   this.kappa_NC.toolTip = "Threshold for eliminating hotpixels."
   this.kappa_NC.onValueUpdated = function (value) {
      engine.kappa = 0+value;
   }

   //

   this.sizer = new VerticalSizer;
   this.sizer.margin = 6;
   this.sizer.spacing = 6;
   this.sizer.add (this.helpLabel);
   this.sizer.addSpacing( 4 );
   this.sizer.add (this.target_GroupBox, 100);
   this.sizer.add (this.kappa_NC);
   this.sizer.add (this.buttons_Sizer);

   this.windowTitle = "KappaSigmaCombine Script";
   this.adjustToContents();
}

kappa_sigma_dialog.prototype = new Dialog;

/*
 * Script entry point.
 */
function main()
{
   // Starting time
   var startts = new Date;

   // Hide the console while our dialog is active.
   console.hide();

   var dialog = new kappa_sigma_dialog();

   for ( ;; ) {
      if (!dialog.execute())
         break;

      // Since this is a relatively slow process, show the console to provide
      // feedback to the user.
      console.show();

      // Allow users to abort execution of this script.
      console.abortEnabled = true;

      // Perform.
      engine.load();
      engine.combine();
      engine.unload();

      // Quit after successful execution.
      break;
   }

   var endts = new Date;
   console.writeln (format( "<end><cbr><br>%.2f s",
                            (endts.getTime() - startts.getTime())/1000));
}

main();
--
 David Serrano

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Javascript: kappa-sigma clipping
« Reply #11 on: 2007 August 27 01:02:39 »
Muy bien David. Tal vez lo de "release early, release often" tendría que inyectármelo yo :)

¿Paralelizar código en JavaScript? Ufff, ¡no sabes lo que pides! Es factible, pero no por el momento; hay muchas otras prioridades ahora. Casi mejor esperar a JS 2.0. Y sí, la respuesta es "write a module" :-) En PCL hay un soporte muy bueno (gracias a la Qt subyacente) de multithreading, como puedes comprobar en el c. fuente de varios módulos.

Sin embargo, tengo que hacer una aclaración importante a este respecto. Aunque no puedes definir threads en JavaScript, la integración del motor de JS que hay en la aplicación principal sí utiliza todos los procesadores disponibles. Por ejemplo, cuando llamas a Image.FFT() o Image.convolution(), o image.atrousWaveletTransform(), las rutinas nativas que estás invocando van a toda leche, porque en realidad estás ejecutando código de PCL. :)

Unas pocas cosillas que he cazado al vuelo. Pongo el código ya modificado. Puede que los números de línea varíen un poco respecto de la versión que has publicado:

153:
Code: [Select]
     console.writeln ( format ("Clipped %u pixels from a total of %u (%.2f%%)",
                                nclipped, h*w*n*N, 100.0*nclipped/(h*w*n*N)) );


Fixed ;)

156:
Code: [Select]
     var window = new ImageWindow (1, 1, n, bps, real,
                                    dstimg.colorSpace != ColorSpace_Gray,
                                    format("kappa_sigma_%.0f", 100.0*this.kappa));


En la función format(), cuando empleas %d se espera recibir un entero. Si lo que se recibe es un número real, se genera un error porque el argumento que se recibe no se corresponde con lo que se quiere representar. Cuando escribí format() mi intención fue emular con la mayor precisión posible el comportamiento de sprintf() en C. Tal vez sea demasiado estricto. El caso es que con ese 100.0 y %.0f te aseguras que no va a haber errores en tiempo de ejecución.

256:
Code: [Select]
     for ( var j = 0; j < previews.length; ++j )
         addViewNode( node, previews[j] );


¿Por qué no estás creando los nodos para los previews como hijos (hojas) de los nodos para las imágenes? Yo creo que es mucho más estructurado hacerlo así.

Hay bastantes más cosas, pero te las comentaré más tarde. Tendría que traducir esto al inglés, pero casi que no, oyes.
Juan Conejero
PixInsight Development Team
http://pixinsight.com/

Offline David Serrano

  • PTeam Member
  • PixInsight Guru
  • ****
  • Posts: 503
Javascript: kappa-sigma clipping
« Reply #12 on: 2007 August 28 14:51:42 »
Quote from: "Juan Conejero"
¿Paralelizar código en JavaScript? Ufff, ¡no sabes lo que pides!


Sí que lo sé :^P. De todas formas, yo sugería la posibilidad de hacer que fuera el usuario el que se tuviera que preocupar de programar para varios procesadores.

Si es el motor el que se encarga de todo, entonces sí que ya sería la bomba :^).


Quote from: "Juan Conejero"
¿Por qué no estás creando los nodos para los previews como hijos (hojas) de los nodos para las imágenes? Yo creo que es mucho más estructurado hacerlo así.


Porque entonces el botón de "Seleccionar todo" no selecciona todo ;^). Y me pareció más fácil cambiar esto que editar el código de la función que selecciona todo. Además, es la idea que tenía principalmente. Por si fuera poco, pienso que cuando un usuario va a sumar imágenes, raramente va a tener definidos varios previews por ahí, por tanto poca cosa habrá que organizar.

La versión 0.4, que está en el horno, tendrá un par de opciones para mostrar sólo imágenes y sólo previews. Así, en el caso típico de N imágenes con un preview idéntico en todas, bastarán un par de clics para seleccionar todo.
--
 David Serrano

Offline David Serrano

  • PTeam Member
  • PixInsight Guru
  • ****
  • Posts: 503
Javascript: kappa-sigma clipping
« Reply #13 on: 2007 August 29 12:47:44 »
Bueno, aquí va la 0.4, esto ya se acerca a algo definitivo. Hay 3 nuevas checkboxes encima de la lista de imágenes, con las que se puede controlar fácilmente qué imágenes se seleccionan.

Well, here goes version 0.4, this is getting nearer something finished. There are 3 new checkboxes above the image list, with which you can easily control what images to select.

Code: [Select]
/*
    KSIntegration.js v0.4 - Image integration with kappa-sigma clipping.
    Copyright (C) 2007  David Serrano, Juan Conejero

    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, version 3 of the License.

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

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

// TODO: write a tutorial

#include <pjsr/Sizer.jsh>
#include <pjsr/FrameStyle.jsh>
#include <pjsr/NumericControl.jsh>
#include <pjsr/TextAlign.jsh>
#include <pjsr/StdButton.jsh>
#include <pjsr/StdIcon.jsh>
#include <pjsr/ColorSpace.jsh>
#include <pjsr/SampleType.jsh>
#include <pjsr/UndoFlag.jsh>

#feature-id KSIntegration
#feature-info Implementation of image combination using kappa-sigma \
  method for rejecting hotpixels. Needs that the images to be combined \
  are previously loaded in the application. They must have the same size \
  and bit depth. Generates a new image with the result of the combination.
// #feature-icon KSIntegration.xpm

#define VERSION "0.4"

function kappa_sigma_engine() {
   this.kappa = 2.0;
   this.imgIDs = new Array;
   this.views = new Array;

   // Populates the views array.
   this.load = function() {
      var N = this.imgIDs.length;
      this.views.length = 0;
      for (var i = 0; i < N; i++) {
         this.views.push (new View (this.imgIDs[i]));
      }
   };

   // Performs an iterative kappa-sigma clipping of a pixel set.
   this.clip = function (set) {
      var stddev;
      var mean;
      var delta;
      var low;
      var high;
      var result;
      // Fill a temporary array with the values that are inside the range
      // specified by kappa. If it ends up the same size as the original
      // one (or if it gets too small), exit. If not, them some values
      // were deleted, so reset the original to not include the deleted
      // ones, and start over again.
      while (1) {
         stddev = Math.stddev (set);
         mean = Math.avg (set);
         delta = this.kappa * stddev;
         low  = mean - delta;
         high = mean + delta;
         result = new Array;  // temp array
         for (var i = 0, n = set.length; i < n; ++i) {
            var v = set[i];
            if (v >= low && v <= high)
               result.push (v);
         }
         // Need at least 3 items to calculate a meaningful standard deviation
         if (result.length < 3)
            return set;
         // if lengths of both sets are equal, all elements are ok: all done
         if (result.length == set.length)
            return result;
         set = result;   // reset the work set with the elements that survived
      }
   }

   // Performs a kappa-sigma integration of the selected views.
   this.combine = function() {
      var N = this.views.length;
      if (N < 3)
         throw Error ("At least three images are required for kappa-sigma integration.");
      var imgs = new Array (N);
      var w = 0;
      var h = 0;
      var n = 0;
      var bps = 0;
      var real = false;
      // initialization of vars and error checking
      for (var i = 0; i < N; i++) {
         if (this.views[i].isNull)
            // does this make any sense? is it ok to retrieve the .fullId of something that .isNull?
            // or could this check be removed, since views comes ultimately from ImageWindows.windows?
            throw Error ("Nonexistent view: " + this.views[i].fullId);
         imgs[i] = this.views[i].image;
         // first pass: initialize vars; next passes: check properties consistency
         if (i == 0) {
            w = imgs[i].width;
            h = imgs[i].height;
            n = imgs[i].numberOfChannels;
         } else {
            if (imgs[i].width != w || imgs[i].height != h || imgs[i].numberOfChannels != n)
               throw Error ("Incongruent image dimension(s): " + this.views[i].fullId);
         }
         bps = Math.max (bps, imgs[i].bitsPerSample);
         real = real || (imgs[i].sampleType != SampleType_Integer);
      }

      var dstimg = new Image (w, h, n,
         (n != 1) ? ColorSpace_RGB : ColorSpace_Gray,
         bps,
         real ? SampleType_Real : SampleType_Integer);
      dstimg.statusEnabled = true;
      dstimg.initializeStatus (format ("Integrating %u images", N), w*h);

      // now, after creating a new image, traverse the ones to be combined and
      // calculate the value for every pixel in every channel
      var clipped;
      var nclipped = 0;
      var pixels = new Array (N);
      for (var iy = 0; iy < h; iy++) {
         for (var ix = 0; ix < w; ix++) {
            for (var ic = 0; ic < n; ic++) {
               for (var i = 0; i < N; i++)
                  pixels[i] = imgs[i].sample (ix, iy, ic);
               clipped = this.clip (pixels);
               dstimg.setSample (Math.avg (clipped), ix, iy, ic);
               if (clipped.length < N)
                  nclipped += N - clipped.length;
            }
         }
         gc();
         dstimg.advanceStatus (w);
      }

      console.writeln (format ("Clipped %u pixels from a total of %u (%.2f%%).",
         nclipped, h*w*n*N, 100.0*nclipped / (h*w*n*N)));
      var window = new ImageWindow (1, 1, n, bps, real,
         dstimg.colorSpace != ColorSpace_Gray,
         format ("kappa_sigma_%.0f", 100.0 * this.kappa));
      var view = window.mainView;
      view.beginProcess (UndoFlag_NoSwapFile); // do not generate any swap file
      view.image.transfer (dstimg);
      view.endProcess();
      window.show();
      window.zoomToFit (false); // don't zoom in
   };
}

var engine = new kappa_sigma_engine();

function kappa_sigma_dialog() {
   this.__base__ = Dialog;
   this.__base__();

   // ----- HELP LABEL

   this.helpLabel = new Label (this);
   this.helpLabel.frameStyle = FrameStyle_Box;
   this.helpLabel.margin = 4;
   this.helpLabel.wordWrapping = true;
   this.helpLabel.useRichText = true;
   this.helpLabel.text = "<b>KSIntegration v"+VERSION+"</b> - A script to integrate " +
      "a set of images by the iterative kappa-sigma rejection method.<br>" +
      "Select the views to be combined and choose the kappa value desired. " +
      "Then click 'Ok' and wait for a new image to be created.";

   // ----- LIST OF TARGETS

   // if all images are checked, check the "All images" checkbox; else, uncheck it
   // ditto previews
   this.updateCBs = function() {
      // start with both marked to be checked and enter a loop
      // if an unckecked image/preview is found, mark the corresponding
      // checkbox to be unchecked.
      var imagesCBstatus = true;
      var previewsCBstatus = true;
      for (i = 0; i < this.dialog.target_List.numberOfChildren; i++) {
         var n = this.dialog.target_List.child (i);
         if (true == n.checked)
            continue;
         if (-1 == n.text (0).indexOf ("->"))   // it's an image
            imagesCBstatus = false;
         else                                   // it's a preview
            previewsCBstatus = false;
         // if both are already marked to be unchecked, there's no need to
         // keep walking the list: exit prematurely
         if (false == imagesCBstatus && false == previewsCBstatus)
            break;
      }
      this.dialog.selectImages_CB.checked = imagesCBstatus;
      this.dialog.selectPreviews_CB.checked = previewsCBstatus;
   }

   this.toggleSimilar_CB = new CheckBox (this);
   this.toggleSimilar_CB.text = "Toggle similar elements";
   this.toggleSimilar_CB.checked = true;
   this.toggleSimilar_CB.toolTip = "Tick to automatically (un)check " +
      "all images or previews that have<br>the same dimensions and bit depth " +
      "as the one that has been just (un)checked.";

   this.target_List = new TreeBox (this);
   this.target_List.setMinSize (400, 200);
   this.target_List.font = new Font ("monospace", 10); // best to show tabulated data
   this.target_List.numberOfColumns = 2;
   this.target_List.headerVisible = true;
   this.target_List.headerSorting = true;
   this.target_List.setHeaderText (0, "Views");
   this.target_List.setHeaderText (1, "Dimensions");
   this.target_List.setHeaderAlignment (0, Align_Left);
   this.target_List.setHeaderAlignment (1, Align_Left);
   // save this function in a visible place
   this.target_List.onNodeUpdated = this.onu = function() {
      var node = this.dialog.target_List.currentNode;
      var foo=0;
      for (bar in node) if (!foo++) {     // beware of the dog!
         // if "Toggle similar" is on, walk the list and set the similar
         // images/previews to the state of the just-clicked one
         if (this.dialog.toggleSimilar_CB.checked) {
            var metadata = node.text (1);

            for (i = 0; i < this.dialog.target_List.numberOfChildren; i++) {
               if (this.dialog.target_List.child (i).text (1) == metadata) {
                  this.dialog.target_List.child (i).checked = node.checked;  // why doesn't this trigger recursion?
               }
            }
         }
         this.dialog.updateCBs();
      }
   }

   // Node creation helper
   function addViewNode (parent, view) {
      var node = new TreeBoxNode (parent);
      node.checkable = true;
      node.checked = false;

      node.setText (0, view.fullId);

      var image = view.image;
      var metadata = format ("%5d x %5d x %d", image.width, image.height, image.numberOfChannels);
      node.setText (1, metadata);

      return node;
   }

   // Build the view tree structure
   var windows = ImageWindow.windows;
   for (var i = 0; i < windows.length; ++i) {
      var node = addViewNode (this.target_List, windows[i].mainView);
      node.expanded = false; // or true to initially expand all preview lists

      var previews = windows[i].previews;
      for (var j = 0; j < previews.length; ++j)
         addViewNode (this.target_List, previews[j]);
   }
   this.target_List.sort();

   // Ensure that all columns are initially visible
   this.target_List.adjustColumnWidthToContents (0);
   this.target_List.adjustColumnWidthToContents (1);

   // ----- CHECKBOXES

   this.selectImages_CB = new CheckBox (this);
   this.selectImages_CB.text = "Select all images";
   this.selectImages_CB.checked = false;
   this.selectImages_CB.toolTip = "Tick to select all entire images.";
   this.selectImages_CB.onCheck = function (checked) {
      this.dialog.target_List.onNodeUpdated = function() {}  // temporarily disable the onNodeUpdated event
      for (var i = 0; i < this.dialog.target_List.numberOfChildren; i++) {
         var str = this.dialog.target_List.child (i).text (0);
         if (-1 == str.indexOf ("->")) {   // this is an image
            this.dialog.target_List.child (i).checked = checked;
         }
      }
      this.dialog.target_List.onNodeUpdated = this.dialog.onu;  // restore the event
   };

   this.selectPreviews_CB = new CheckBox (this);
   this.selectPreviews_CB.text = "Select all previews";
   this.selectPreviews_CB.checked = false;
   this.selectPreviews_CB.toolTip = "Tick to select all previews.";
   this.selectPreviews_CB.onCheck = function (checked) {
      this.dialog.target_List.onNodeUpdated = function() {}  // temporarily disable the onNodeUpdated event
      for (var i = 0; i < this.dialog.target_List.numberOfChildren; i++) {
         var str = this.dialog.target_List.child (i).text (0);
         if (-1 != str.indexOf ("->")) {   // this is a preview
            this.dialog.target_List.child (i).checked = checked;
         }
      }
      this.dialog.target_List.onNodeUpdated = this.dialog.onu;  // restore the event
   }

   this.checkBoxes_Sizer = new HorizontalSizer;
   this.checkBoxes_Sizer.spacing = 4;
   this.checkBoxes_Sizer.add (this.selectImages_CB);
   this.checkBoxes_Sizer.add (this.selectPreviews_CB);
   this.checkBoxes_Sizer.addStretch();
   this.checkBoxes_Sizer.add (this.toggleSimilar_CB);

   this.target_GroupBox = new GroupBox (this);
   this.target_GroupBox.title = "Images to integrate";
   this.target_GroupBox.sizer = new VerticalSizer;
   this.target_GroupBox.sizer.margin = 4;
   this.target_GroupBox.sizer.spacing = 4;
   this.target_GroupBox.sizer.add (this.checkBoxes_Sizer);
   this.target_GroupBox.sizer.add (this.target_List, 100);

   // ----- KAPPA NUMERIC CONTROL

   this.kappa_NC = new NumericControl (this);
   this.kappa_NC.label.text = "Kappa value:";
   this.kappa_NC.setRange (0.1, 10);
   this.kappa_NC.setPrecision (2);
   this.kappa_NC.setValue (engine.kappa);
   this.kappa_NC.toolTip = "Threshold for eliminating hotpixels."
   this.kappa_NC.onValueUpdated = function (value) {
      engine.kappa = 0 + value;   // javascript nuisances
      //engine.kappa = value.toInt;  // maybe better!!
   }

   // ----- BUTTONS

   this.ok_Button = new PushButton (this);
   this.ok_Button.text = " OK ";
   // transfer the names of the selected images to the engine
   this.ok_Button.onClick = function() {
      // couldn't achieve this using this.dialog.target_List.selectedNodes, its length is always 1
      for (n = 0; n < this.dialog.target_List.numberOfChildren; n++)
         if (this.dialog.target_List.child (n).checked)
            engine.imgIDs.push (this.dialog.target_List.child (n).text (0));
      this.dialog.ok();
   };

   this.cancel_Button = new PushButton (this);
   this.cancel_Button.text = " Cancel ";
   this.cancel_Button.onClick = function() {
      this.dialog.cancel();
   };

   this.buttons_Sizer = new HorizontalSizer;
   this.buttons_Sizer.spacing = 4;
   this.buttons_Sizer.addStretch();
   this.buttons_Sizer.add (this.ok_Button);
   this.buttons_Sizer.add (this.cancel_Button);

   // ----- PACK EVERYTHING

   this.sizer = new VerticalSizer;
   this.sizer.margin = 6;
   this.sizer.spacing = 6;
   this.sizer.add (this.helpLabel);
   this.sizer.addSpacing (4);
   this.sizer.add (this.target_GroupBox, 100);
   this.sizer.add (this.kappa_NC);
   this.sizer.add (this.buttons_Sizer);

   this.windowTitle = "KSIntegration v" + VERSION;
   this.adjustToContents();
}

kappa_sigma_dialog.prototype = new Dialog;

function main() {
   // used for computing elapsed time
   var startts = new Date;

   // Hide the console while our dialog is active.
   console.hide();

   var dialog = new kappa_sigma_dialog();

   for (;;) {
      if (!dialog.execute())
         break;

      console.show();
      console.abortEnabled = true;

      engine.load();
      engine.combine();

      // Quit after successful execution.
      break;
   }

   var endts = new Date;
   console.writeln (format ("<end><cbr><br>%.2f s",
      (endts.getTime() - startts.getTime()) / 1000));
}

main();
--
 David Serrano

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Javascript: kappa-sigma clipping
« Reply #14 on: 2007 September 04 01:15:52 »
Hi David,

You're making a good job. Keep it running.

A few remarks on your code:

Code: [Select]
        if (this.views[i].isNull)
            // does this make any sense? is it ok to retrieve the .fullId of something that .isNull?
            // or could this check be removed, since views comes ultimately from ImageWindows.windows?
            throw Error ("Nonexistent view: " + this.views[i].fullId);


Yes it could be removed for the reason you noted.

This is a good example of defensive programming: the error check doesn't hurt in this context because the code involved is not time-critical, and leaving there protects us from ourselves (because it stresses on a common error condition), if for example we reuse this code fragment later. I've learned to code defensively over the years... I think that our own errors model our programming style more than anything else.

Code: [Select]
                 this.dialog.target_List.child (i).checked = node.checked;  // why doesn't this trigger recursion?

Because the PixInsight/PCL platform (which ultimately is under your JavaScript code) protects your code :)

This is true for all controls in the PCL and PJSR frameworks: event handlers are only triggered when the user generates events. Changing a control property programmatically never triggers an event.

Code: [Select]
     this.dialog.target_List.onNodeUpdated = function() {}  // temporarily disable the onNodeUpdated event
      for (var i = 0; i < this.dialog.target_List.numberOfChildren; i++) {
         var str = this.dialog.target_List.child (i).text (0);
         if (-1 != str.indexOf ("->")) {   // this is a preview
            this.dialog.target_List.child (i).checked = checked;
         }
      }
      this.dialog.target_List.onNodeUpdated = this.dialog.onu;  // restore the event


Why are you saving/disabling/restoring the onNodeUpdated event handler function? For the reason we've seen in the previous point, changing the checked property of TreeBoxNode programmatically won't trigger the TreeBox.onNodeUpdated event, so there's no need to care.

Or are you experiencing spurious events triggered in this case? If so, then you've found a bug that must be fixed immediately. Please let me know.

Code: [Select]
  this.kappa_NC.onValueUpdated = function (value) {
      engine.kappa = 0 + value;   // javascript nuisances
      //engine.kappa = value.toInt;  // maybe better!!
   }


What's this?

Code: [Select]
  this.ok_Button.onClick = function() {
      // couldn't achieve this using this.dialog.target_List.selectedNodes, its length is always 1
      for (n = 0; n < this.dialog.target_List.numberOfChildren; n++)
         if (this.dialog.target_List.child (n).checked)
            engine.imgIDs.push (this.dialog.target_List.child (n).text (0));
      this.dialog.ok();
   };


Then you've found a bug. Of course selectedNodes should be giving you the whole set of selected nodes on the TreeBox. I'll revise this and fix as necessary.

On a side note, I still think that a version of the script to integrate disk image files would be great, not excluding (but complementing) what you're doing here.
Juan Conejero
PixInsight Development Team
http://pixinsight.com/