3D star profile / Perfil estelar en 3D (BETA)

David Serrano

Well-known member
_3dplot.png


Click on a small image (not beyond 100x100) to make it active and run the script. Previews are OK.

Hacer clic en una imagen peque?a (no mucho m?s de 100x100) para hacerla activa, y ejecutar el script. Se pueden usar previews.

Code:
#include <pjsr/UndoFlag.jsh>
#include <pjsr/FillRule.jsh>

var xform_cache = [];
function xform_coords (x, y, z) {
    var k = format ('%05d%05d%6.5f', x, y, z);
    if (xform_cache[k]) { return xform_cache[k]; }

    var angle = 10/180*Math.PI;
    var alt = 60/180*Math.PI;
    var new_x = x; var origin_x = 14;
    var new_y = y; var origin_y = 14;
    var scale = 7;

    // rotation
    var mod = Math.sqrt (
        Math.pow (Math.abs (origin_x - x), 2) +
        Math.pow (Math.abs (origin_y - y), 2)
    );
    var cur_angle = Math.atan2 (origin_y - new_y, new_x - origin_x);
    //console.writeln ('got x (',x,') y (',y,') mod (',mod,') cur_angle (',(cur_angle/Math.PI*180),')');
    new_x = origin_x + mod * Math.cos(cur_angle - angle);
    new_y = origin_y - mod * Math.sin(cur_angle - angle);
    //console.writeln ('new x (',new_x,') y (',new_y,')');
    //console.writeln ('--');

    // scaling and perspective
    new_x *= scale;
    new_y *= scale / (Math.PI/2 / alt);  // alt == 90/2?  perspective = scale/2
    
    // translation
    new_x += 180;
    new_y += 150;

    // z
    new_y -= scale * scale * scale * z * Math.cos (alt);

    return xform_cache[k] = new Point (new_x, new_y);
}

var pixel_cache = [];
function getpixel (i, x, y) {
    var k = format ('%05d%05d', x, y);
    if (pixel_cache[k]) { return pixel_cache[k]; }

    return pixel_cache[k] = Math.avg (
        i.sample (x, y, 0),
        i.sample (x, y, 1),
        i.sample (x, y, 2)
    );
}

var src = ImageWindow.activeWindow.currentView.image;
var w = new ImageWindow (12*src.width, 12*src.height, 3, 8, false, true, '_3dplot');
var v = w.currentView;
var i = v.image;

// setup
var white_pen   = new Pen (0xffffffff);
var black_pen   = new Pen (0xff000000);
var red_pen     = new Pen (0xffff0000);
var white_brush = new Brush (0xffffffff);
var black_brush = new Brush (0xff000000);
var red_brush   = new Brush (0xffff0000);

var bmp = new Bitmap (i.width, i.height);
var g = new Graphics (bmp);
v.beginProcess (UndoFlag_PixelData);
    bmp.fill (0xff008000);
    g.pen = red_pen; g.brush = white_brush;
        
    for (var n = 0; n < src.height-1; n++) {
        for (var m = 0; m < src.width-1; m++) {
            var p1 = xform_coords (m,   n,   getpixel (src, m,   n  ));
            var p2 = xform_coords (m+1, n,   getpixel (src, m+1, n  ));
            var p3 = xform_coords (m+1, n+1, getpixel (src, m+1, n+1));
            var p4 = xform_coords (m,   n+1, getpixel (src, m,   n+1));
            g.drawPolygon (new Array (p1, p2, p3, p4));
        }
    }
    i.blend (bmp);
    g.end();
v.endProcess();
w.show();

I didn't study in the university and this is the first time I try this kind of things, so I'd like to apologize to all those who read the code and take any offense at it ;).

No he ido a la universidad y es la primera vez que intento hacer esta clase de cosas, as? pues pido disculpas a aquellos que lean el c?digo y se vean profundamente ofendidos por ?l ;).
 
Bravo David, absolutely wonderful work :D

It is amazing how a 3D look at the image can show so many things, so conspicuously --things that are barely visible on the real 2D image. This script can have lots of exciting applications. For example, how does a light-pollution gradient look like? Or how a bout a before/after comparison for a denoising routine such as ACDNR or GREYCstoration?

I love it :) And *of course* I'd like to include it for 1.5...

I didn't study in the university and this is the first time I try this kind of things, so I'd like to apologize to all those who read the code and take any offense at it

Nor did I. So no offense at all. And indeed not bad for your first time ;)
 
Hi again,

Here's a quick and interesting test with David's 3D plot script.

Original image
test1.png


The image after a strong application of a wavelet-based noise reduction routine (one of the new tools in version 1.5):
test2.png


The plots (no need to specify which is which :) ):

_3dplot1.png


_3dplot2.png


This is really interesting and useful. For example, we can evaluate the features of the new noise reduction algorithm and the suitability of a particular application much better on the 3D graphs. Great! (thanks David!) 8)
 
Hi,

I would replace this code:
Code:
var mod = Math.sqrt (
        Math.pow (Math.abs (origin_x - x), 2) +
        Math.pow (Math.abs (origin_y - y), 2)
    );
with
Code:
var mod = Math.sqrt ((origin_x - x)*(origin_x - x)+(origin_y - y)*(origin_y - y));

This substitution should make the script faster.

Also, the index in xform_cache could be made using only the (x,y) coordinates. The z coordinate is redundant. This way the string of the index can be shorter and perhaps the searches faster.

I hope this helps.

Andr?s.
 
Hi again,

I have been working a bit on this script and I have written a new version with this improvements:
  • 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. In my computer a 500x500 image is processed in less than 6 seconds.
Code:
#include <pjsr/UndoFlag.jsh>
#include <pjsr/FillRule.jsh>


function xform_coords (x, y, z) {
    var angle = 10/180*Math.PI;
    var alt = 60/180*Math.PI;
    var scaleXY = 7;
    var scaleZ = 350;

    // rotation
    var mod = Math.sqrt(x*x+y*y);
    var cur_angle = Math.atan2 (-y, x);
    var new_x = mod * Math.cos(cur_angle - angle);
    var new_y = -mod * Math.sin(cur_angle - angle);

    // scaling and perspective
    new_x *= scaleXY;
    new_y *= scaleXY / (Math.PI/2 / alt);  // alt == 90/2?  perspective = scale/2
   
    // z
    new_y -= scaleZ * z * Math.cos (alt);
    return new Point (new_x, new_y);
}

// This function computes the rectangle covered by the 3D image
function CalculateLimits(image)
{
   // The median of the source image is the level of the "floor" of the 3D image
   var stats = new ImageStatistics;
   stats.generate(image);
   var median=stats.median;

   var p1=xform_coords(0,0,median);
   var p2=xform_coords(image.width-1,0,median);
   var p3=xform_coords(0,image.height-1,median);
   var p4=xform_coords(image.width-1,image.height-1,median);

   var r=new Rect();
   r.left=Math.min(Math.min(p1.x,p2.x),Math.min(p3.x,p4.x));
   r.top=Math.min(Math.min(p1.y,p2.y),Math.min(p3.y,p4.y));
   r.right=Math.max(Math.max(p1.x,p2.x),Math.max(p3.x,p4.x));
   r.bottom=Math.max(Math.max(p1.y,p2.y),Math.max(p3.y,p4.y));
   return r;
}

// Extract the luminance of the source image
var src;
if(ImageWindow.activeWindow.currentView.image.numberOfChannels!=1)
{
   src =new Image();
   ImageWindow.activeWindow.currentView.image.extractLuminance(src);
} else {
   src = ImageWindow.activeWindow.currentView.image;
}

// Compute the rectangle of the 3D image in order to size the destination image
var limits=CalculateLimits(src);

var w = new ImageWindow (Math.round(limits.width), Math.round(limits.height), 3, 8, false, true, '_3dplot');
var v = w.currentView;
var i = v.image;

// Start of the process
var startts = new Date;
var bmp = new Bitmap (i.width, i.height);
var g = new Graphics (bmp);
v.beginProcess (UndoFlag_PixelData);
   bmp.fill (0xff606060);
   g.pen =  new Pen (0xff000000);
   g.brush = new Brush (0xffffffff);

   // Calculates the coordinates of each pixel applying the perspective transformation
   var xform=[];
   for (var n = 0; n < src.height; n++) {
      xform[n]=[];
      for (var m = 0; m < src.width; m++) {
         xform[n][m]=xform_coords (m, n, src.sample(m, n));
         xform[n][m].x-=limits.left;
         xform[n][m].y-=limits.top;
      }
   }

   // 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 p1 = ((xform[n])[m]);
         var p2 = ((xform[n])[m+1]);
         var p3 = ((xform[n+1])[m+1]);
         var p4 = ((xform[n+1])[m]);
         g.drawPolygon (new Array (p1, p2, p3, p4));
      }
   }
   g.end();
   i.blend (bmp);
v.endProcess();
w.show();
var endts = new Date;
console.writeln(format ("
3D view: %.2f s",(endts.getTime() - startts.getTime()) / 1000));
 
Juan, that's a wonderful NR algorithm... the background is completely flattened as the 3D plot reveals ;). Of course, it will be a honour to see this included in PixInsight 1.5. My question about receiving events was related to this script. The original idea was to monitor an image and update the plot whenever it changed.

Andr?s, your improvements are awesome. Thank you very much for contributing them! :).

I'm feeling too lazy right now to implement a couple of remaining things:

- Confirmation if the image is too large. It's too easy to define a preview and run the script without activating the preview, thus running it on the entire image.
- User interface.
 
Excellent, and very useful.

One thing: the generated 3dplot image can't draw the total height source image upper stars. Is there a easy way to show entire profiles modifying script code?
 
[quote author="C. Sonnenstein"] Is there a easy way to show entire profiles modifying script code?[/quote]
Yes, I'm working on it.
 
I have a new version with the following improvements:
  • The resulting image shows the full extent of the 3D view.
  • The code has been reorganized and structured. There is now an object with the generation parameters.
  • A MTF is applied to the image to improve the visualization. I don't know how to get the value of the screen transfer function active in the source image, so I've had to hard code it in the algorithm. A user interface dialog would be nice :wink:
  • The perspective transformation has been simplified again.
  • There is a couple of checks before starting the algorithm.

3dview.png


Code:
#include <pjsr/UndoFlag.jsh>
#include <pjsr/FillRule.jsh>

// Creates the parameters object.
function DefaultParams()
{
   var params={};
   params.angle = 30;   // Perspective angle in degrees
   params.scaleXY = 6;  // Scale factor for X and Y axis
   params.scaleZ = 500; // Scale factor for Z axis
   params.mtf=0.1;      // Midtones transfer value (0,1)
   params.backgroundColor=0xff606060;
   params.polygonFill=0xffffffff;
   params.polygonBorder=0xff000000;

   // Precalculated sin and cos of angle
   params.sin_angle=Math.sin(params.angle*Math.PI/180);
   params.cos_angle=Math.cos(params.angle*Math.PI/180);
   return params;
}

// Perspective transformation
function Perspective(x, y, z, params)
{
   with(params){
      // x
      var new_x = (x-y*sin_angle) * scaleXY;

      // y
      var new_y = y*cos_angle * scaleXY;

      // z
      new_y -= scaleZ * z;
   }
   return new Point (new_x, new_y);
}

// Applies the perspective transformation to all the pixels in the image
function CreatePerspective(src, limits, perspecParams)
{
   // Calculates the coordinates of each pixel applying the perspective transformation
   var xform=[];
   limits.left=limits.top=1e10;
   limits.right=limits.bottom=-1e10;
   for (var y = 0; y < src.height; y++) {
      xform[y]=[];
      for (var x = 0; x < src.width; x++){
         var z=Math.mtf(perspecParams.mtf,src.sample(x,y));
         var pixel=Perspective(x, y, z, perspecParams);
         xform[y][x]=pixel;
         if(pixel.x<limits.left)   limits.left=pixel.x;
         if(pixel.x>limits.right)  limits.right=pixel.x;
         if(pixel.y<limits.top)    limits.top=pixel.y;
         if(pixel.y>limits.bottom) limits.bottom=pixel.y;
      }
   }
   return xform;
}

function ShowError(message)
{
   var msgb=new MessageBox(message,"Error generating 3D view");
   msgb.execute();
   return;
}

function main()
{
   if(ImageWindow.activeWindow.currentView.isNull)
      return ShowError("There is not an active image");
   if(ImageWindow.activeWindow.currentView.image.width>500 ||
      ImageWindow.activeWindow.currentView.image.height>500)
         return ShowError("The image is bigger than 500x500 pixels");
         
   var startts = new Date;

   // Extract the luminance of the source image
   var src;
   if(ImageWindow.activeWindow.currentView.image.numberOfChannels!=1)
   {
      src =new Image();
      ImageWindow.activeWindow.currentView.image.extractLuminance(src);
   } else
      src = ImageWindow.activeWindow.currentView.image;

   // Inits the parameters
   var perspecParams=DefaultParams();

   // Create the perspective
   var limits=new Rect;
   var xform=CreatePerspective(src,limits,perspecParams);
   //with(limits) console.writeln(format("%f %f %f %f",left,top,right,bottom));

   // Create the output window
   var w = new ImageWindow (Math.round(limits.width), Math.round(limits.height), 3, 8, false, true, '_3dplot');
   var v = w.currentView;
   var i = v.image;

   // Start of the process
   var bmp = new Bitmap (i.width, i.height);
   var g = new Graphics (bmp);

   // Applies a translation to the graphics to center the image
   g.translateTransformation(-limits.left,-limits.top);
   
   v.beginProcess(UndoFlag_NoSwapFile);
      bmp.fill (perspecParams.backgroundColor);
      g.pen =  new Pen (perspecParams.polygonBorder);
      g.brush = new Brush (perspecParams.polygonFill);

      // 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 polygon=new Array(
               xform[n][m],
               xform[n][m+1],
               xform[n+1][m+1],
               xform[n+1][m]);
            g.drawPolygon (polygon);
         }
      }
      g.end();
      i.blend (bmp);
   v.endProcess();
   w.show();
   
   var endts = new Date;
   console.writeln(format ("
3D view: %.2f s",(endts.getTime() - startts.getTime()) / 1000));
}

main();
 
I have added an optional shading effect:
3dview.png

Code:
#include <pjsr/UndoFlag.jsh>
#include <pjsr/FillRule.jsh>

// Creates the parameters object.
function DefaultParams()
{
   var params={};
   params.angle = 30;   // Perspective angle in degrees
   params.scaleXY = 6;  // Scale factor for X and Y axis
   params.scaleZ = 500; // Scale factor for Z axis
   params.mtf=0.1;      // Midtones transfer value (0,1)
   params.backgroundColor=0xff403000;
   params.polygonFill=0xffffffff;
   params.polygonBorder=0x10000000;
   params.useShading=true; // Apply shading effect
   params.lightBrightness=700; // Light intensity for the shading effect

   // Precalculated sin and cos of angle
   params.sin_angle=Math.sin(params.angle*Math.PI/180);
   params.cos_angle=Math.cos(params.angle*Math.PI/180);
   return params;
}

// Perspective transformation
function Perspective(x, y, z, params)
{
   with(params){
      // x
      var new_x = (x-y*sin_angle) * scaleXY;

      // y
      var new_y = y*cos_angle * scaleXY;

      // z
      new_y -= scaleZ * z;
   }
   return new Point (new_x, new_y);
}

// Applies the perspective transformation to all the pixels in the image
function CreatePerspective(src, limits, perspecParams)
{
   // Calculates the coordinates of each pixel applying the perspective transformation
   var xform=[];
   limits.left=limits.top=1e10;
   limits.right=limits.bottom=-1e10;
   for (var y = 0; y < src.height; y++) {
      xform[y]=[];
      for (var x = 0; x < src.width; x++){
         var pixel=Perspective(x, y, src.sample(x,y), perspecParams);
         xform[y][x]=pixel;
         if(pixel.x<limits.left)   limits.left=pixel.x;
         if(pixel.x>limits.right)  limits.right=pixel.x;
         if(pixel.y<limits.top)    limits.top=pixel.y;
         if(pixel.y>limits.bottom) limits.bottom=pixel.y;
      }
   }
   return xform;
}

function ApplyMTF(src, mtf)
{
   // Calculates the coordinates of each pixel applying the perspective transformation
   for (var y = 0; y < src.height; y++)
      for (var x = 0; x < src.width; x++){
         var z=Math.mtf(mtf,src.sample(x,y));
         src.setSample(z,x,y);
      }
}

function ShowError(message)
{
   var msgb=new MessageBox(message,"Error generating 3D view");
   msgb.execute();
   return;
}

function main()
{
   if(ImageWindow.activeWindow.currentView.isNull)
      return ShowError("There is not an active image");
   if(ImageWindow.activeWindow.currentView.image.width>500 ||
      ImageWindow.activeWindow.currentView.image.height>500)
         return ShowError("The image is bigger than 500x500 pixels");
         
   var startts = new Date;

   // Extract the luminance of the source image
   var src =new Image();
   ImageWindow.activeWindow.currentView.image.extractLuminance(src);

   // Inits the parameters
   var perspecParams=DefaultParams();

   ApplyMTF(src, perspecParams.mtf);
   
   // Create the perspective
   var limits=new Rect;
   var xform=CreatePerspective(src,limits,perspecParams);
   //with(limits) console.writeln(format("%f %f %f %f",left,top,right,bottom));

   // Create the output window
   var w = new ImageWindow (Math.round(limits.width), Math.round(limits.height), 3, 8, false, true, '_3dplot');
   var v = w.currentView;
   var i = v.image;

   // Start of the process
   var bmp = new Bitmap (i.width, i.height);
   var g = new Graphics (bmp);

   // Applies a translation to the graphics to center the image
   g.translateTransformation(-limits.left,-limits.top);
   
   v.beginProcess(UndoFlag_NoSwapFile);
      bmp.fill (perspecParams.backgroundColor);
      g.pen =  new Pen (perspecParams.polygonBorder);
      var fillbrush=[];
      if(perspecParams.useShading){
         for(var c=0; c<256;++c)
            fillbrush[c]=new Brush(0xFF000000 | (c<<16) | (c<<8) | c);
      }else
         g.brush = new Brush (perspecParams.polygonFill);

      // 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++) {
            if(perspecParams.useShading){
               var slope=src.sample(m,n)-src.sample(m+1,n)+
                         src.sample(m,n+1)-src.sample(m+1,n+1);
               var color=Math.round(slope * perspecParams.lightBrightness + 128);
               color=Math.max(0,Math.min(255,color));
               g.brush=fillbrush[color];
            }
            
            var polygon=new Array(
               xform[n][m],
               xform[n][m+1],
               xform[n+1][m+1],
               xform[n+1][m]);
            g.drawPolygon (polygon);
         }
      }
      g.end();
      i.blend (bmp);
   v.endProcess();
   w.show();
   
   var endts = new Date;
   console.writeln(format ("
3D view: %.2f s",(endts.getTime() - startts.getTime()) / 1000));
}

main();
 
This is terrific work! Would it be worthwhile to re-implement this is PCL so it's compiled code rather than javascript?
 
Hi Andr?s,

Very nice! You're doing a fantastic work. Congratulations to both!

This script can be developed into something very powerful and useful. A user interface to define parameters would be nice. Also, you could add false color to improve visual detection of image features, and a nonlinear representation to enhance the dimmest parts of the image (for example, a deep-sky object is represented with a too low profile, and this script would be perfect to represent the profile of a galaxy for example.

I'd remove the error test at line #82. I'm generating 3D profiles for images of 1Kx1K pixels and larger without problems.

Andr?s, do you agree with including this script in PI 1.5 distribution (the latest version available at the date of release) ?

Would it be worthwhile to re-implement this is PCL so it's compiled code rather than javascript?

Of course, Sander. As PCL/C++ code with some optimizations (generation of a downsampled representation, multithreaded implementation as an interruptible process), this script could run in nearly real-time. It could be an observer tool just like Statistics.

However, the JavaScript runtime in PixInsight has a great advantage over compiled C++ modules: you can develop a tool with JavaScript without exiting the PixInsight platform, with all the advantages of an interpreted language backed by a powerful, native runtime. We have used JavaScript to develop several important tools that were implemented in C++ after thorough testing as interpreted routines. An example is HDRWaveletTransform.
 
Right, I can see how a javascript module can be a great prototyping tool. I was actually considering writing a profile tool in PCL (just installed it) but this 3D mesh would be much nicer. I agree that having this work like a 'monitor' is very useful.

Well, maybe I'll write the profile tool anyway, as an exercise. With a single line it's easy to represent colors, in 3D that is more tricky because it gets cluttered.
 
[quote author="Juan Conejero"]
This script can be developed into something very powerful and useful. A user interface to define parameters would be nice. Also, you could add false color to improve visual detection of image features, and a nonlinear representation to enhance the dimmest parts of the image (for example, a deep-sky object is represented with a too low profile, and this script would be perfect to represent the profile of a galaxy for example.
[/quote]
The nonlinear scaling is done with the MTF that I added to the last version. A good default value would be the midtone value of the Screen Transfer Function of the image (if I knew how to get it).
[quote author="Juan Conejero"]
I'd remove the error test at line #82. I'm generating 3D profiles for images of 1Kx1K pixels and larger without problems.
[/quote]
The limit could perhaps be increased, however, since the process can not be aborted, I think there should be a limit. Not everybody has a machine like yours :D.
[quote author="Juan Conejero"]
Andr?s, do you agree with including this script in PI 1.5 distribution (the latest version available at the date of release) ?[/quote]
I have no problem with that. Of course, the original idea is of David Serrano and he should be who granted the permission.
 
Sander,

I look forward to see that profiling tool! :) Please don't hesitate to ask for any help/information you may need.
 
A few quick modifications:

- The script can now be aborted by the user. So a size warning/error is no longer needed (IMO).

- The script shows progress information on the console.

- The perspective transformation points are now calculated faster. See changes in the CreatePerspective function (line 44).

- The MTF transformation is now carried out using a HistogramTransformation instance on a temporary copy of the working image. See the ApplyMTF function (line 73).

Code:
#include <pjsr/UndoFlag.jsh>
#include <pjsr/FillRule.jsh>
#include <pjsr/ColorSpace.jsh>

// Creates the parameters object.
function DefaultParams()
{
   var params={};
   params.angle = 30;   // Perspective angle in degrees
   params.scaleXY = 6;  // Scale factor for X and Y axis
   params.scaleZ = 500; // Scale factor for Z axis
   params.mtf=0.1;      // Midtones transfer value (0,1)
   params.backgroundColor=0xff403000;
   params.polygonFill=0xffffffff;
   params.polygonBorder=0x10000000;
   params.useShading=true; // Apply shading effect
   params.lightBrightness=700; // Light intensity for the shading effect

   // Precalculated sin and cos of angle
   params.sin_angle=Math.sin(params.angle*Math.PI/180);
   params.cos_angle=Math.cos(params.angle*Math.PI/180);
   return params;
}

// Perspective transformation -- Replaced by faster inline code; see below
/*
function Perspective(x, y, z, params)
{
   with(params){
      // x
      var new_x = (x - ys) * scaleXY;

      // y
      var new_y = ycs - scaleZ * z;

      // z
      new_y -= scaleZ * z; // -----> ???
   }
   return new Point (new_x, new_y);
}
*/

// Applies the perspective transformation to all the pixels in the image
function CreatePerspective(src, limits, perspecParams)
{
   src.initializeStatus( "Computing 3D coordinates", src.height );

   // Calculates the coordinates of each pixel applying the perspective transformation
   var xform=[];
   limits.left=limits.top=1e10;
   limits.right=limits.bottom=-1e10;
   for (var y = 0; y < src.height; y++) {
      xform[y]=[];
      var ys = y * perspecParams.sin_angle;
      var ycs = y * perspecParams.cos_angle * perspecParams.scaleXY;
      for (var x = 0; x < src.width; x++) {
         var z = src.sample( x, y );
         var pixel = new Point( (x - ys)*perspecParams.scaleXY,
                                ycs - perspecParams.scaleZ*z ); 
         //Perspective(x, ys, ycs, src.sample(x,y), perspecParams);
         xform[y][x]=pixel;
         if(pixel.x<limits.left)   limits.left=pixel.x;
         if(pixel.x>limits.right)  limits.right=pixel.x;
         if(pixel.y<limits.top)    limits.top=pixel.y;
         if(pixel.y>limits.bottom) limits.bottom=pixel.y;
      }

      src.advanceStatus( 1 );
   }
   return xform;
}

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

   /*
   // Calculates the coordinates of each pixel applying the perspective transformation
   for (var y = 0; y < src.height; y++)
      for (var x = 0; x < src.width; x++){
         var z=Math.mtf(mtf,src.sample(x,y));
         src.setSample(z,x,y);
      }
   */
}

function DieError(message)
{
   var msgb = new MessageBox(message,"Error generating 3D view");
   msgb.execute();
   throw Error( message );
}

function main()
{
   if (ImageWindow.activeWindow.currentView.isNull)
      DieError("There is not an active image");
   /*
   if(ImageWindow.activeWindow.currentView.image.width>500 ||
      ImageWindow.activeWindow.currentView.image.height>500)
         DieError("The image is bigger than 500x500 pixels");*/

   console.show();
   console.writeln( "<end><cbr>
***** 3D Profiling Script *****" );
   console.flush();

   var startts = new Date;

   // Extract the luminance of the source image
   var src = new Image();
   ImageWindow.activeWindow.currentView.image.extractLuminance(src);

   // Inits the parameters
   var perspecParams = DefaultParams();

   // Apply a midtones transformation
   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;

   // Create the perspective
   var limits = new Rect;
   var xform = CreatePerspective(src,limits,perspecParams);
   //with(limits) console.writeln(format("%f %f %f %f",left,top,right,bottom));

   // Create the output window
   var w = new ImageWindow (Math.round(limits.width), Math.round(limits.height), 3, 8, false, true, '_3dplot');
   var v = w.currentView;
   var i = v.image;

   // Start of the process
   var bmp = new Bitmap (i.width, i.height);
   var g = new Graphics (bmp);

   // Applies a translation to the graphics to center the image
   g.translateTransformation(-limits.left,-limits.top);

   src.initializeStatus( "Rendering 3D profile", src.height-1 );

   v.beginProcess(UndoFlag_NoSwapFile);
      bmp.fill (perspecParams.backgroundColor);
      g.pen =  new Pen (perspecParams.polygonBorder);
      var fillbrush=[];
      if(perspecParams.useShading){
         for(var c=0; c<256;++c)
            fillbrush[c]=new Brush(0xFF000000 | (c<<16) | (c<<8) | c);
      }else
         g.brush = new Brush (perspecParams.polygonFill);

      // 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++) {
            if(perspecParams.useShading){
               var slope=src.sample(m,n)-src.sample(m+1,n)+
                         src.sample(m,n+1)-src.sample(m+1,n+1);
               var color=Math.round(slope * perspecParams.lightBrightness + 128);
               color=Math.max(0,Math.min(255,color));
               g.brush=fillbrush[color];
            }
           
            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);
   v.endProcess();
   w.show();
   
   var endts = new Date;
   console.writeln(format ("
3D view: %.2f s",(endts.getTime() - startts.getTime()) / 1000));
}

main();
 
[quote author="Andres.Pozo"]I have added an optional shading effect:[/quote]

Diossss qu? bueno!!! xD

I've seen you removed the parameter "alt" I had in my original script. It was meant to be a configurable value that indicated the altitude over the terrain, indicated in degress. Thus, a value of 0 would render the grid as if we were on the ground (untested!) and a value of 90 would give a 2D grid.


[quote author="Andres.Pozo"]
Code:
         return ShowError("The image is bigger than 500x500 pixels");
[/quote]

I agree with Juan on this. The image dimensions limit I suggested was meant to be some kind of "Are you sure?" dialog, to avoid someone ;) asking for a higher limit. I like to allow users to crash their machine if they want to. Detecting the amount of RAM in the system would be useful to make the limit dynamic.

I would implement it as an absolute number of pixels (width * height), thus allowing the user to plot a 2000x10 image.


[quote author="Andres.Pozo"]Of course, the original idea is of David Serrano and he should be who granted the permission.[/quote]

The idea may be mine, but at this point the code is yours ;).
 
I have merged the improvements of Juan in the code that I was writting. I also have implemented the visualization using false color mapping the height to a color palette.

Example of false color with shading:
3dview1.png


Example of false color without shading:
3dview2.png


Code:
#include <pjsr/UndoFlag.jsh>
#include <pjsr/FillRule.jsh>
#include <pjsr/ColorSpace.jsh>

// Creates the parameters object.
function DefaultParams()
{
   var params={};
   params.angle = 30;   // Perspective angle in degrees
   params.scaleXY = 6;  // Scale factor for X and Y axis
   params.scaleZ = 500; // Scale factor for Z axis
   params.mtf=0.1;      // Midtones transfer value (0,1)
   params.backgroundColor=0xff403000;
   params.polygonFill=0xffffffff;
   params.polygonBorder=0x80000000;
   params.useShading=true; // Apply shading effect
   params.lightBrightness=2000; // Light intensity for the shading effect
   params.palette = [ [0x80,0,0] , [0,0x80,0] , [0,0,0xC0], [0x80,0,0x80], [0x80,0x80,0], [0x80,0x80,0x80] ];
   
   // Precalculated sin and cos of angle
   params.sin_angle=Math.sin(params.angle*Math.PI/180);
   params.cos_angle=Math.cos(params.angle*Math.PI/180);
   return params;
}

// Applies the perspective transformation to all the pixels in the image
function CreatePerspective(src, limits, perspecParams)
{
   src.initializeStatus( "Computing 3D coordinates", src.height );

   // Calculates the coordinates of each pixel applying the perspective transformation
   var xform=[];
   limits.left=limits.top=1e10;
   limits.right=limits.bottom=-1e10;
   for (var y = 0; y < src.height; y++) {
      xform[y]=[];
      var ys = y * perspecParams.sin_angle;
      var ycs = y * perspecParams.cos_angle * perspecParams.scaleXY;
      for (var x = 0; x < src.width; x++){
         var z= src.sample(x,y);
         var pixel=new Point( (x - ys)*perspecParams.scaleXY,
                                ycs - perspecParams.scaleZ*z );
         xform[y][x]=pixel;
         if(pixel.x<limits.left)   limits.left=pixel.x;
         if(pixel.x>limits.right)  limits.right=pixel.x;
         if(pixel.y<limits.top)    limits.top=pixel.y;
         if(pixel.y>limits.bottom) limits.bottom=pixel.y;
      }
      src.advanceStatus( 1 );
   }
   return xform;
}

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

function RenderImage(src,i,limits,xform,perspecParams)
{
   // 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);

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

   // The median of the source image is the level of the "floor" of the 3D image
   var stats = new ImageStatistics;
   stats.generate(src);
   var minz=stats.median;

   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 z=Math.max(Math.max(z1,z2),Math.max(z3,z4));
         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 main()
{
   if(ImageWindow.activeWindow.currentView.isNull)
      return DieError("There is not an active image");

   console.show();
   console.writeln( "<end><cbr>
***** 3D Profiling Script *****" );
   console.flush();

   var startts = new Date;

   // Extract the luminance of the source image
   var src =new Image();
   ImageWindow.activeWindow.currentView.image.extractLuminance(src);

   // Inits the parameters
   var perspecParams=DefaultParams();

   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;

   // Create the perspective
   var limits=new Rect;
   var xform=CreatePerspective(src,limits,perspecParams);
   //with(limits) console.writeln(format("%f %f %f %f",left,top,right,bottom));

   // Create the output window
   var newid;
   if(ImageWindow.activeWindow.currentView.isPreview)
      newid=ImageWindow.activeWindow.mainView.id+'_'+ImageWindow.activeWindow.currentView.id+'_3dplot';
   else
      newid=ImageWindow.activeWindow.currentView.id+'_3dplot';
   var w = new ImageWindow (Math.round(limits.width), Math.round(limits.height), 3, 8, false, true, newid);
   try{
      var v = w.currentView;
      var i = v.image;

      v.beginProcess(UndoFlag_NoSwapFile);
      RenderImage(src,i,limits,xform,perspecParams);
      v.endProcess();

      w.show();

      var endts = new Date;
      console.writeln(format ("
3D view: %.2f s",(endts.getTime() - startts.getTime()) / 1000));
   } catch(err)
   {
      w.close();
      console.writeln(err);
   }
}

main();
 
[quote author="David Serrano"]
I've seen you removed the parameter "alt" I had in my original script. It was meant to be a configurable value that indicated the altitude over the terrain, indicated in degress. Thus, a value of 0 would render the grid as if we were on the ground (untested!) and a value of 90 would give a 2D grid.
[/quote]

Hi,

I have reimplemented the possibility of rotating the 3D view. It is implemented using a linear projection matrix so it is quite fast.

These are a couple of screenshots:
3dview1.png

3dview2.png

Code:
#include <pjsr/UndoFlag.jsh>
#include <pjsr/FillRule.jsh>
#include <pjsr/ColorSpace.jsh>

// Creates the parameters object.
function DefaultParams()
{
   var params={};
   params.azimut = 30;    // Perspective angle in degrees
   params.elevation = 20; // Perspective elevation angle
   params.scaleXY = 6;    // Scale factor for X and Y axis
   params.scaleZ = 50;   // Scale factor for Z axis
   params.mtf=0.5;        // Midtones transfer value (0,1)
   params.backgroundColor=0xff403000;
   params.polygonFill=0xffffffff;
   params.polygonBorder=0x80000000;
   params.useShading=true; // Apply shading effect
   params.lightBrightness=2000; // Light intensity for the shading effect
   params.palette = [ [0x80,0,0] , [0,0x80,0] , [0,0,0xC0], [0x80,0,0x80], [0x80,0x80,0], [0,0x80,0x80], [0x80,0x80,0x80] ];

   return params;
}

// Applies the perspective transformation to all the pixels in the image
function CreatePerspective(src, limits, perspecParams, minz)
{
   src.initializeStatus( "Computing 3D coordinates", src.height );

   // Calculates the coordinates of each pixel applying the perspective transformation
   var xform=[];
   limits.left=limits.top=1e10;
   limits.right=limits.bottom=-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.azimut);
   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);

   for (var y = 0; y < src.height; y++) {
      xform[y]=[];
      for (var x = 0; x < src.width; x++){
         var z= src.sample(x,y)*zscale;
         var pixel=new Point( (m00*x+m10*y)*perspecParams.scaleXY,
                              (m01*x+m11*y+m21*z)*perspecParams.scaleXY );
         xform[y][x]=pixel;
         if(pixel.x<limits.left)   limits.left=pixel.x;
         if(pixel.x>limits.right)  limits.right=pixel.x;
         if(pixel.y<limits.top)    limits.top=pixel.y;
         if(pixel.y>limits.bottom) limits.bottom=pixel.y;
      }
      src.advanceStatus( 1 );
   }
   
   return xform;
}

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

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

   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 main()
{
   if(ImageWindow.activeWindow.currentView.isNull)
      return DieError("There is not an active image");

   console.show();
   console.writeln( "<end><cbr>
***** 3D Profiling Script *****" );
   console.flush();

   var startts = new Date;

   // Extract the luminance of the source image
   var src =new Image();
   ImageWindow.activeWindow.currentView.image.extractLuminance(src);

   // Inits the parameters
   var perspecParams=DefaultParams();

   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 stats = new ImageStatistics;
   stats.generate(src);
   var minz=stats.median;

   // Create the perspective
   var limits=new Rect;
   var xform=CreatePerspective(src,limits,perspecParams,minz);
   //with(limits) console.writeln(format("%f %f %f %f",left,top,right,bottom));

   // Create the output window
   var newid;
   if(ImageWindow.activeWindow.currentView.isPreview)
      newid=ImageWindow.activeWindow.mainView.id+'_'+ImageWindow.activeWindow.currentView.id+'_3dplot';
   else
      newid=ImageWindow.activeWindow.currentView.id+'_3dplot';
   var w = new ImageWindow (Math.round(limits.width), Math.round(limits.height), 3, 8, false, true, newid);
   try{
      var v = w.currentView;
      var i = v.image;

      v.beginProcess(UndoFlag_NoSwapFile);
      RenderImage(src,i,limits,xform,perspecParams,minz);
      v.endProcess();

      w.show();

      var endts = new Date;
      console.writeln(format ("
3D view: %.2f s",(endts.getTime() - startts.getTime()) / 1000));
   } catch(err)
   {
      w.close();
      console.writeln(err);
   }
}

main();
 
Back
Top