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.
/*
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();