New version with improved dialog and a few optimizations.
One of the changes is somewhat subtle but very important. In previous versions a function was used to calculate 3D rendition points and the limits of the rendered scene, namely:
function CreatePerspective(src, limits, perspecParams, minz)
The problem is that the limits argument was intended to be passed by reference, but that is impossible: in JavaScript, function parameters are always passed by value.
Luckily, limits was working as a by-reference argument (or the script wouldn't work at all), but this is only due to some peculiarities of my implementation (the way I've embedded Mozilla's JavaScript engine into PI Core, and how I've connected some internal PI objects to expose them through the JS runtime).
For this reason, I have replaced the old CreatePerspective function by a _3dplot_xform object that owns both the transformation's point cube (the points member) and the transformation's bounding rectangle (the limits member).
/*
3DPlot v1.0
A script to generate three-dimensional image renditions.
Copyright (C) 2009 Andrés Pozo, 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/>.
*/
/*
Changelog:
1.0: Several improvements to the user interface and a few optimizations.
Replaced CreatePerspective() by a _3dplot_xform object.
Some identifiers changed for improved naming coherency.
0.9: License, changelog, bare-bones user interface.
0.8: Smooth color transitions.
0.7: Reimplemented the possibility of rotating the 3D view.
0.6: Visualization now maps the height to a color palette.
0.5: The script can now be aborted by the user.
The script shows progress information on the console.
The perspective transformation points are now calculated faster.
The MTF transformation is now carried out using a HT.
0.4: Optional shading effect added.
0.3: The resulting image shows the full extent of the 3D view.
The code has been reorganized and structured.
A MTF is applied to the image to improve the visualization.
The perspective transformation has been simplified again.
There is a couple of checks before starting the algorithm.
0.2: It now works on grayscale images.
The 3D image is centered in the output image.
The code is a bit simpler.
It is now 7 times faster.
0.1: Initial test version.
*/
#define TITLE "3DPlot"
#define VERSION "1.0"
#include <pjsr/ColorSpace.jsh>
#include <pjsr/FillRule.jsh>
#include <pjsr/FrameStyle.jsh>
#include <pjsr/NumericControl.jsh>
#include <pjsr/UndoFlag.jsh>
#feature-id 3DPlot
#feature-info Please write something here.
//#feature-icon 3dplot.xpm
// Creates the parameters object.
function _3dplot_params()
{
this.srcView = ImageWindow.activeWindow.currentView;
this.azimuth = 30; // Perspective angle in degrees
this.elevation = 20; // Perspective elevation angle
this.scaleXY = 6; // Scale factor for X and Y axis
this.scaleZ = 50; // Scale factor for Z axis
this.mtf=0.5; // Midtones transfer value (0,1)
this.backgroundColor=0xff403000;
this.polygonFill=0xffffffff;
this.polygonBorder=0x80000000;
this.useShading=true; // Apply shading effect
this.lightBrightness=2000; // Light intensity for the shading effect
this.palette_elems = 256;
this.palette = [ [0x80,0,0] , [0,0x80,0] , [0,0,0xC0], [0x80,0,0x80], [0x80,0x80,0], [0,0x80,0x80], [0x80,0x80,0x80] ];
}
var perspecParams = new _3dplot_params;
// Applies the perspective transformation to all the pixels in the image
function _3dplot_xform(src, perspecParams, minz)
{
// Calculates the coordinates of each pixel applying the perspective transformation
this.points=new Array;
this.limits=new Rect( 1e10, 1e10, -1e10, -1e10 );
// Modify the zscale in order to normalize the peak height
// when the mtf changes
var zscale=perspecParams.scaleZ/(1-minz);
// Projection matrix
var alpha=Math.rad(90-perspecParams.elevation);
var beta=Math.rad(perspecParams.azimuth);
var m00=Math.cos(beta);
var m10=-Math.sin(beta);
var m20=0;
var m01=Math.cos(alpha)*Math.sin(beta);
var m11=Math.cos(alpha)*Math.cos(beta);
var m21=-Math.sin(alpha)*zscale;
src.initializeStatus( "Computing 3D coordinates", src.height );
for (var y = 0; y < src.height; y++) {
this.points[y]=new Array;
var m10y = m10*y;
var m11y = m11*y;
for (var x = 0; x < src.width; x++){
var z = src.sample(x,y);
var p = new Point( m00*x + m10y, m01*x + m11y + m21*z );
p.mul( perspecParams.scaleXY );
this.points[y][x] = p;
if (p.x < this.limits.left) this.limits.left = p.x;
if (p.x > this.limits.right) this.limits.right = p.x;
if (p.y < this.limits.top) this.limits.top = p.y;
if (p.y > this.limits.bottom) this.limits.bottom = p.y;
}
src.advanceStatus( 1 );
}
gc();
}
function ApplyMTF(src, mtf)
{
// Faster solution using a process instance
var HT = new HistogramTransformation;
with ( HT )
{
H = // c0, m, c1, r0, r1
[[0, 0.5, 1, 0, 1],
[0, 0.5, 1, 0, 1],
[0, 0.5, 1, 0, 1],
[0, mtf, 1, 0, 1],
[0, 0.5, 1, 0, 1]];
}
var wtmp = new ImageWindow( 1, 1, 1, 16, false, src.colorSpace != ColorSpace_Gray );
var v = wtmp.mainView;
v.beginProcess( UndoFlag_NoSwapFile );
v.image.assign( src );
v.endProcess();
HT.executeOn( v, false ); // no swap file
src.assign( v.image );
wtmp.close();
}
function DieError(message)
{
var msgb=new MessageBox(message,"Error generating 3D view");
msgb.execute();
throw Error( message );
}
function ApplyLighting(basecolor, light)
{
var r=(basecolor[0]*light/1000);
r=r>255?255:r;
var g=(basecolor[1]*light/1000);
g=g>255?255:g;
var b=(basecolor[2]*light/1000);
b=b>255?255:b;
return 0xFF000000 | (r<<16)| (g<<8) | b;
}
// the resulting palette doesn't have to be exactly 'num' elements long.
function ExpandPalette(params)
{
var p = params.palette;
var num = params.palette_elems;
var expanded = new Array;
var steps = Math.round (num / p.length); // steps between two provided palette elements
for (var c = 0; c < p.length - 1; c++) {
var from = p[c];
var to = p[c+1];
var step_red = (to[0] - from[0]) / steps;
var step_green = (to[1] - from[1]) / steps;
var step_blue = (to[2] - from[2]) / steps;
for (s = 0; s < steps; s++) {
var new_red = from[0] + s * step_red;
var new_green = from[1] + s * step_green;
var new_blue = from[2] + s * step_blue;
expanded.push ([ new_red, new_green, new_blue ]);
}
}
// add last user-supplied palette element
expanded.push (p[p.length-1]);
params.palette = expanded;
}
function RenderImage(src,i,limits,xform,perspecParams,minz)
{
// Start of the process
var bmp = new Bitmap (i.width, i.height);
var g = new Graphics (bmp);
g.antialiasing=true;
// Applies a translation to the graphics to center the image
g.translateTransformation(-limits.left,-limits.top);
bmp.fill (perspecParams.backgroundColor);
ExpandPalette (perspecParams);
var fillbrush=[];
if(!perspecParams.useShading){
for(var c=0;c<perspecParams.palette.length;c++)
fillbrush[c]=new Brush(0xFF000000 | (perspecParams.palette[c][0]<<16) | (perspecParams.palette[c][1]<<8) | perspecParams.palette[c][2]);
g.pen = new Pen (perspecParams.polygonBorder,0);
} else
// Using shading the polygon borders should be less visible
g.pen = new Pen ((perspecParams.polygonBorder&0x00FFFFFF) | 0x20000000);
src.initializeStatus( "Rendering 3D profile", src.height-1 );
// Paint the polygons from back to front
for (var n = 0; n < src.height-1; n++) {
for (var m = 0; m < src.width-1; m++) {
var z1=src.sample(m,n),
z2=src.sample(m+1,n),
z3=src.sample(m,n+1),
z4=src.sample(m+1,n+1);
// Get the palette index
//var z=(z1+z2+z3+z4)/4;
var zM=Math.max(Math.max(z1,z2),Math.max(z3,z4));
var zm=Math.min(Math.min(z1,z2),Math.min(z3,z4));
var z=(zm+zM)/2;
z=(z-minz)/(1-minz); //rescale for mapping the palete to (minz,1)
var palidx=Math.round(z*perspecParams.palette.length);
palidx=palidx<0 ? 0 : (palidx>=perspecParams.palette.length ? perspecParams.palette.length-1 : palidx);
if(perspecParams.useShading){
// Get the lighting factor
var slope=z1-z2+z3-z4;
var lighting=(slope+0.5)*perspecParams.lightBrightness;
lighting=lighting<0 ? 0 : Math.round(lighting);
g.brush=new Brush(ApplyLighting(perspecParams.palette[palidx], lighting));
} else
g.brush=fillbrush[palidx];
var polygon=new Array(
xform[n][m],
xform[n][m+1],
xform[n+1][m+1],
xform[n+1][m]);
g.drawPolygon (polygon);
}
src.advanceStatus( 1 );
}
g.end();
i.blend (bmp);
}
function _3dplot_dialog() {
this.__base__ = Dialog;
this.__base__();
var labelWidth1 = this.font.width( "Polygon border color:" );
var editWidth1 = 9*this.font.width( "0" );
var editWidth2 = 12*this.font.width( "0" );
// help label
this.helpLabel = new Label (this);
with (this.helpLabel) {
frameStyle = FrameStyle_Box;
margin = 4;
wordWrapping = true;
useRichText = true;
text = "<p><b>" + TITLE + " v" + VERSION + "</b> — A " +
"script to generate three-dimensional image renditions.</p>" +
"<p>Copyright © 2009 Andrés Pozo / David Serrano / Juan Conejero</p>";
}
// source image
this.srcImage_Label = new Label( this );
this.srcImage_Label.minWidth = labelWidth1;
this.srcImage_Label.text = "Source image:";
this.srcImage_Label.textAlignment = TextAlign_Right|TextAlign_VertCenter;
this.srcImage_ViewList = new ViewList( this );
this.srcImage_ViewList.minWidth = 250;
this.srcImage_ViewList.getAll(); // include main views as well as previews
this.srcImage_ViewList.currentView = perspecParams.srcView;
this.srcImage_ViewList.toolTip = "<p>Select the image to be rendered.</p>";
this.srcImage_ViewList.onViewSelected = function( view )
{
perspecParams.srcView = view;
};
this.srcImage_Sizer = new HorizontalSizer;
this.srcImage_Sizer.spacing = 4;
this.srcImage_Sizer.add( this.srcImage_Label );
this.srcImage_Sizer.add( this.srcImage_ViewList, 100 );
// azimuth
this.azimuth_NC = new NumericControl (this);
with (this.azimuth_NC) {
real = false;
label.text = "Azimuth:";
label.minWidth = labelWidth1;
setRange (0, 90);
slider.setRange (0, 90);
slider.minWidth = 250;
edit.minWidth = editWidth1;;
setValue (perspecParams.azimuth);
toolTip = "<p>Azimuth angle in degrees.</p>";
onValueUpdated = function (value) {
perspecParams.azimuth = value;
}
}
// elevation
this.elevation_NC = new NumericControl (this);
with (this.elevation_NC) {
real = false;
label.text = "Elevation:";
label.minWidth = labelWidth1;
setRange (0, 90);
slider.setRange (0, 90);
slider.minWidth = 250;
edit.minWidth = editWidth1;;
setValue (perspecParams.elevation);
toolTip = "<p>Observer's elevation angle above the ground, in degrees.</p>";
onValueUpdated = function (value) {
perspecParams.elevation = value;
}
}
// scaleXY
this.scaleXY_NC = new NumericControl (this);
with (this.scaleXY_NC) {
real = false;
label.text = "X-Y plane scale:";
label.minWidth = labelWidth1;
setRange (1, 10);
slider.setRange (1, 10);
slider.minWidth = 250;
edit.minWidth = editWidth1;;
setValue (perspecParams.scaleXY);
toolTip = "<p>Scale of X and Y coordinates in rendition pixels per image pixels.</p>";
onValueUpdated = function (value) {
perspecParams.scaleXY = value;
}
}
// scaleZ
this.scaleZ_NC = new NumericControl (this);
with (this.scaleZ_NC) {
real = false;
label.text = "Z-axis scale:";
label.minWidth = labelWidth1;
setRange (1, 100);
slider.setRange (1, 100);
slider.minWidth = 250;
edit.minWidth = editWidth1;;
setValue (perspecParams.scaleZ);
toolTip = "<p>Scaling factor of Z-axis coordinates.</p>";
onValueUpdated = function (value) {
perspecParams.scaleZ = value;
}
}
// mtf
this.mtf_NC = new NumericControl (this);
with (this.mtf_NC) {
label.text = "Midtones balance:";
label.minWidth = labelWidth1;
setRange (0, 1);
slider.setRange (1, 500);
slider.minWidth = 250;
setPrecision (3);
edit.minWidth = editWidth1;;
setValue (perspecParams.mtf);
toolTip = "<p>This parameter is part of a histogram transformation applied to " +
"improve faint detail visualization.</p>";
onValueUpdated = function (value) {
perspecParams.mtf = value;
}
}
// lightBrightness
this.brightness_NC = new NumericControl (this);
with (this.brightness_NC) {
real = false;
label.text = "Brightness:";
label.minWidth = labelWidth1;
setRange (100, 5000);
slider.setRange (1, 50);
slider.minWidth = 250;
edit.minWidth = editWidth1;;
setValue (perspecParams.lightBrightness);
toolTip = "<p>Intensity of incident light.</p>";
onValueUpdated = function (value) {
perspecParams.lightBrightness = value;
}
}
// useShading
this.shading_CB = new CheckBox (this);
with (this.shading_CB) {
text = "Use shading";
checked = perspecParams.useShading;
onCheck = function (checked) { perspecParams.useShading = checked; }
toolTip = "<p>If this option is selected, a special shading algorithm will be used " +
"to enhance the 3-D rendition.</p>";
}
this.shading_Sizer = new HorizontalSizer;
this.shading_Sizer.addSpacing( labelWidth1+4 );
this.shading_Sizer.add( this.shading_CB );
this.shading_Sizer.addStretch();
// background color
this.backgroundColor_Label = new Label( this );
this.backgroundColor_Label.text = "Background color:";
this.backgroundColor_Label.textAlignment = TextAlign_Right|TextAlign_VertCenter;
this.backgroundColor_Label.minWidth = labelWidth1;
this.backgroundColor_Edit = new Edit( this );
this.backgroundColor_Edit.text = format( "%X", perspecParams.backgroundColor );
this.backgroundColor_Edit.setFixedWidth( editWidth2 );
this.backgroundColor_Edit.toolTip = "<p>Background color encoded as a 32-bit hexadecimal integer.<br/>" +
"(AARRGGBB format: AA=alpha (transparency), RR=red, GG=green, BB=blue)</p>";
this.backgroundColor_Edit.onEditCompleted = function()
{
perspecParams.backgroundColor = parseInt( this.text, 16 );
this.text = format( '%X', perspecParams.backgroundColor );
};
this.backgroundColor_Sizer = new HorizontalSizer;
this.backgroundColor_Sizer.spacing = 4;
this.backgroundColor_Sizer.add( this.backgroundColor_Label );
this.backgroundColor_Sizer.add( this.backgroundColor_Edit );
this.backgroundColor_Sizer.addStretch();
// polygon fill color
this.polygonFill_Label = new Label( this );
this.polygonFill_Label.text = "Polygon fill color:";
this.polygonFill_Label.textAlignment = TextAlign_Right|TextAlign_VertCenter;
this.polygonFill_Label.minWidth = labelWidth1;
this.polygonFill_Edit = new Edit( this );
this.polygonFill_Edit.text = format( "%X", perspecParams.polygonFill );
this.polygonFill_Edit.setFixedWidth( editWidth2 );
this.polygonFill_Edit.toolTip = "<p>Polygon fill color encoded as a 32-bit hexadecimal integer.<br/>" +
"(AARRGGBB format: AA=alpha (transparency), RR=red, GG=green, BB=blue)</p>";
this.polygonFill_Edit.onEditCompleted = function()
{
perspecParams.polygonFill = parseInt( this.text, 16 );
this.text = format( '%X', perspecParams.polygonFill );
};
this.polygonFill_Sizer = new HorizontalSizer;
this.polygonFill_Sizer.spacing = 4;
this.polygonFill_Sizer.add( this.polygonFill_Label );
this.polygonFill_Sizer.add( this.polygonFill_Edit );
this.polygonFill_Sizer.addStretch();
// polygon border color
this.polygonBorder_Label = new Label( this );
this.polygonBorder_Label.text = "Polygon border color:";
this.polygonBorder_Label.textAlignment = TextAlign_Right|TextAlign_VertCenter;
this.polygonBorder_Label.minWidth = labelWidth1;
this.polygonBorder_Edit = new Edit( this );
this.polygonBorder_Edit.text = format( "%X", perspecParams.polygonBorder );
this.polygonBorder_Edit.setFixedWidth( editWidth2 );
this.polygonBorder_Edit.toolTip = "<p>Polygon border color encoded as a 32-bit hexadecimal integer.<br/>" +
"(AARRGGBB format: AA=alpha (transparency), RR=red, GG=green, BB=blue)</p>";
this.polygonBorder_Edit.onEditCompleted = function()
{
perspecParams.polygonBorder = parseInt( this.text, 16 );
this.text = format( '%X', perspecParams.polygonBorder );
};
this.polygonBorder_Sizer = new HorizontalSizer;
this.polygonBorder_Sizer.spacing = 4;
this.polygonBorder_Sizer.add( this.polygonBorder_Label );
this.polygonBorder_Sizer.add( this.polygonBorder_Edit );
this.polygonBorder_Sizer.addStretch();
// ### TODO: Better color selection controls - Include a ColorSelectionDialog object in PJSR?
// ### TODO: Allow selecting palettes
// buttons
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 = 8;
this.buttons_Sizer.addStretch();
this.buttons_Sizer.add (this.ok_Button);
this.buttons_Sizer.add (this.cancel_Button);
// pack everything
this.sizer = new VerticalSizer;
with (this.sizer) {
margin = 8;
spacing = 6;
add (this.helpLabel);
addSpacing (4);
add (this.srcImage_Sizer);
add (this.azimuth_NC);
add (this.elevation_NC);
add (this.scaleXY_NC);
add (this.scaleZ_NC);
add (this.mtf_NC);
add (this.brightness_NC);
add (this.shading_Sizer);
add (this.backgroundColor_Sizer);
add (this.polygonFill_Sizer);
add (this.polygonBorder_Sizer);
add (this.buttons_Sizer);
}
this.windowTitle = TITLE + " v" + VERSION;
this.adjustToContents();
this.setFixedSize();
}
_3dplot_dialog.prototype = new Dialog;
function FullViewIdAsValidViewId( fullId )
{
return fullId.replace( "->", "_" );
}
function main()
{
if(ImageWindow.activeWindow.isNull)
DieError("There is no active image");
console.hide();
for ( ;; )
{
if (!(new _3dplot_dialog).execute())
return;
if ( !perspecParams.srcView.isNull )
break;
(new MessageBox( "No source view has been selected!",
TITLE, StdIcon_Error, StdButton_Ok )).execute();
}
console.show();
console.writeln( "<end><cbr><br>***** 3D Profiling Script *****" );
console.flush();
var startts = new Date;
// Extract the luminance of the source image
var src = new Image();
perspecParams.srcView.image.extractLuminance(src);
if( perspecParams.mtf!=0.5)
ApplyMTF(src, perspecParams.mtf);
// Allow the user to abort this script
console.abortEnabled = true;
// Allow status monitoring for our working image
src.statusEnabled = true;
// The median of the source image is the level of the "floor" of the 3D image
var minz=src.median();
// Create the perspective
var xform=new _3dplot_xform(src, perspecParams, minz);
//with(xform.limits) console.writeln(format("%f %f %f %f",left,top,right,bottom));
// Create the output window
var newid = FullViewIdAsValidViewId( perspecParams.srcView.fullId ) + "_3dplot";
var w = new ImageWindow (Math.round(xform.limits.width),
Math.round(xform.limits.height), 3, 8, false, true, newid);
try{
var v = w.mainView;
var i = v.image;
v.beginProcess(UndoFlag_NoSwapFile);
RenderImage(src, i, xform.limits, xform.points, perspecParams, minz);
v.endProcess();
w.show();
var endts = new Date;
console.writeln(format ("<br />3D view: %.2f s",(endts.getTime() - startts.getTime()) / 1000));
} catch(err)
{
w.close();
console.writeln(err);
}
}
main();