package ij.plugin;
import ij.*;
import ij.plugin.filter.*;
import ij.process.*;
import ij.gui.*;
import java.awt.*;
import java.awt.image.*;
import java.math.*;
import java.util.*;
import ij.measure.*;
public class SurfacePlotter implements PlugIn {
static final int fontSize = 14;
static int plotWidth = 350;
static int polygonMultiplier = 100;
static boolean oneToOne;
static boolean firstTime = true;
static boolean showWireframe=false;
static boolean showGrayscale=true;
static boolean showAxis=true;
static boolean whiteBackground=false;
static boolean blackFill=false;
static boolean smooth = true;
ImagePlus img;
int[] x,y;
boolean invertedLut;
double angleInDegrees = 35;
double angle = (angleInDegrees/360.0)*2.0*Math.PI;
double angle2InDegrees = 15.0;
double angle2 = (angle2InDegrees/360.0)*2.0*Math.PI;
double yinc2 = Math.sin(angle2);
double p1x, p1y; double p2x, p2y; double p3x, p3y;
LookUpTable lut;
public void run(String arg) {
img = WindowManager.getCurrentImage();
if (img==null)
{IJ.noImage(); return;}
if (img.getType()==ImagePlus.COLOR_RGB)
{IJ.error("Surface Plotter", "Grayscale or pseudo-color image required"); return;}
invertedLut = img.getProcessor().isInvertedLut();
if (firstTime) {
if (invertedLut)
whiteBackground = true;
firstTime = false;
}
if (!showDialog())
return;
int stackFlags = IJ.setupDialog(img, 0);
if(stackFlags == PlugInFilter.DONE)
return;
Date start = new Date();
lut = img.createLut();
if (stackFlags==PlugInFilter.DOES_STACKS && img.getStack().getSize()>1){
ImageStack stackSource = img.getStack();
ImageProcessor ip = stackSource.getProcessor(1);
ImageProcessor plot = makeSurfacePlot(ip);
ImageStack stack = new ImageStack(plot.getWidth(), plot.getHeight());
stack.setColorModel(plot.getColorModel());
for (int i=1;i<=stackSource.getSize();i++)
stack.addSlice(null, plot.duplicate().getPixels());
stack.setPixels(plot.getPixels(), 1);
ImagePlus plots = new ImagePlus("Surface Plot", stack);
plots.show();
for (int i=2;i<=stackSource.getSize();i++) {
IJ.showStatus("Drawing slice " + i + "..." + " (" + (100*(i-1)/stackSource.getSize()) + "% done)");
ip = stackSource.getProcessor(i);
plot = makeSurfacePlot(ip);
ImageWindow win = plots.getWindow();
if (win!=null && win.isClosed()) break;
stack.setPixels(plot.getPixels(), i);
plots.setSlice(i);
}
} else {
ImageProcessor plot = makeSurfacePlot(img.getProcessor());
new ImagePlus("Surface Plot", plot).show();
}
Date end = new Date();
long lstart = start.getTime();
long lend = end.getTime();
long difference = lend - lstart;
IJ.register(SurfacePlotter.class);
IJ.showStatus("Done in "+difference+" msec." );
}
boolean showDialog() {
GenericDialog gd = new GenericDialog("Surface Plotter");
gd.addNumericField("Polygon Multiplier (10-200%):", polygonMultiplier, 0);
gd.addCheckbox("Draw_Wireframe", showWireframe);
gd.addCheckbox("Shade", showGrayscale);
gd.addCheckbox("Draw_Axis", showAxis);
gd.addCheckbox("Source Background is Lighter", whiteBackground);
gd.addCheckbox("Fill Plot Background with Black", blackFill);
gd.addCheckbox("One Polygon Per Line", oneToOne);
gd.addCheckbox("Smooth", smooth);
gd.showDialog();
if (gd.wasCanceled())
return false;
polygonMultiplier = (int)gd.getNextNumber();
showWireframe = gd.getNextBoolean();
showGrayscale = gd.getNextBoolean();
showAxis = gd.getNextBoolean();
whiteBackground = gd.getNextBoolean();
blackFill = gd.getNextBoolean();
oneToOne = gd.getNextBoolean();
smooth = gd.getNextBoolean();
if (showWireframe && !showGrayscale)
blackFill = false;
if (polygonMultiplier>400) polygonMultiplier = 400;
if (polygonMultiplier<10) polygonMultiplier = 10;
return true;
}
public ImageProcessor makeSurfacePlot(ImageProcessor ip) {
ip = ip.duplicate();
Rectangle roi = img.getProcessor().getRoi();
ip.setRoi(roi);
if (!(ip instanceof ByteProcessor)) {
ip.setMinAndMax(img.getProcessor().getMin(), img.getProcessor().getMax());
ip = ip.convertToByte(true);
ip.setRoi(roi);
}
double angle = (angleInDegrees/360.0)*2.0*Math.PI;
int polygons = (int)(plotWidth*(polygonMultiplier/100.0)/4);
if (oneToOne)
polygons = roi.height;
double xinc = 0.8*plotWidth*Math.sin(angle)/polygons;
double yinc = 0.8*plotWidth*Math.cos(angle)/polygons;
IJ.showProgress(0.01);
ip.setInterpolate(!oneToOne);
ip = ip.resize(plotWidth, polygons);
int width = ip.getWidth();
int height = ip.getHeight();
double min = ip.getMin();
double max = ip.getMax();
if(invertedLut) ip.invert();
if(whiteBackground) ip.invert();
if (smooth) ip.smooth();
x = new int[width+2];
y = new int[width+2];
double xstart = 10.0;
if (xinc<0.0)
xstart += Math.abs(xinc)*polygons;
ByteProcessor ipProfile =new ByteProcessor(width, (int)(256+width*yinc2));
ipProfile.setValue(255);
ipProfile.fill();
double ystart = yinc2*width;
int ybase = (int)(ystart+0.5);
int windowWidth =(int)(plotWidth+polygons*Math.abs(xinc) + 20.0);
int windowHeight = (int)(ipProfile.getHeight()+polygons*yinc + 10.0);
if(showAxis){
xstart += 50+20;
ystart += 10;
windowWidth += 60+20;
windowHeight += 20;
p1x = xstart;
p1y = ystart+255;
p2x = xstart+xinc*height;;
p2y = p1y+yinc*height;
p3x = p2x+width-1;
p3y = p2y- yinc2*width;
}
if(showGrayscale) {
int v;
int[] column = new int[255];
for(int row=0; row<255; row++) {
if(whiteBackground)
v = row;
else
v = 255-row;
column[row] = v;
}
int base = ipProfile.getHeight()-255;
for(int col=0; col<width; col++) {
ipProfile.putColumn(col, base-(int)(yinc2*col+0.5), column, 255);
}
} else {
ipProfile.setValue(254);
ipProfile.fill();
}
ipProfile.snapshot();
ImageProcessor ip2 = new ByteProcessor(windowWidth, windowHeight);
if(showGrayscale) {
ip2.setColorModel(ip.getColorModel());
if(invertedLut)
ip2.invertLut();
fixLut(ip2);
}
if(!blackFill)
ip2.setValue(255);
else
ip2.setValue(0);
ip2.fill();
for (int row=0; row<height; row++) {
double[] profile = ip.getLine(0, row, width-1, row);
clearAboveProfile(ipProfile, profile, width, yinc2);
int ixstart = (int)(xstart+0.5);
int iystart = (int)(ystart+0.5);
ip2.copyBits(ipProfile, ixstart, iystart-ybase, Blitter.COPY_TRANSPARENT);
ipProfile.reset();
if (showWireframe) {
ip2.setValue(0);
double ydelta = 0.0;
ip2.moveTo(ixstart, (int)(ystart+255.5 - profile[0]) );
for(int i=1; i<width; i++) {
ydelta += yinc2;
ip2.lineTo( ixstart+i, (int) (ystart+255.5-(profile[i]+ydelta)));
}
ip2.drawLine(ixstart, iystart+255, ixstart + width-1, (int)( ystart+255.5-ydelta) );
ip2.drawLine( ixstart, iystart+255-(int) (profile[0]+0.5), ixstart, iystart+255 );
ip2.drawLine( ixstart+width-1, (int) ( ystart+255.5-ydelta), ixstart+width-1, (int) (ystart+255.5-(profile[width-1]+ydelta)) );
}
xstart += xinc;
ystart += yinc;
if ((row%10)==0) IJ.showProgress((double)row/height);
}
IJ.showProgress(1.0);
if(invertedLut) {
ip.invert();
ip.invertLut();
}
if(whiteBackground)
ip.invert();
if (showAxis) {
if (!lut.isGrayscale() && showGrayscale)
ip2 = ip2.convertToRGB();
drawAndLabelAxis(ip, ip2, roi);
}
if (img.getStackSize()==1)
ip2 = trimPlot(ip2, ybase);
return ip2;
}
void drawAndLabelAxis(ImageProcessor ip, ImageProcessor ip2, Rectangle roi) {
ip2.setFont(new Font("SansSerif", Font.PLAIN, fontSize));
if(!blackFill)
ip2.setColor(Color.black);
else
ip2.setColor(Color.white);
ip2.setAntialiasedText(true);
String s;
int w, h;
Calibration cal = img.getCalibration();
s = cal.getValueUnit();
if (s.equals("Gray Value"))
s = "";
w = ip2.getFontMetrics().stringWidth(s);
drawAxis(ip2, (int) p1x, (int) p1y-255, (int) p1x, (int) p1y , s, 10, -1, 0, 1);
double min, max;
if (img.getBitDepth()==8) {
min = 0;
max = 255;
} else {
min = img.getProcessor().getMin();
max = img.getProcessor().getMax();
}
if (cal.calibrated()) {
min = cal.getCValue((int)min);
max = cal.getCValue((int)max);
}
ip2.setAntialiasedText(true);
s = String.valueOf( (double) Math.round(max*10)/10);
w = ip.getFontMetrics().stringWidth(s);
h = ip.getFontMetrics().getHeight();
ip2.drawString(s, (int) p1x-18-w, (int) p1y-255 +h/2); s = String.valueOf( (double) Math.round(min*10)/10);
w = ip2.getFontMetrics().stringWidth(s);
ip2.drawString(s, (int) p1x-18-w, (int) p1y +h/2);
boolean unitsMatch = cal.getXUnit().equals(cal.getYUnit());
String xunits = unitsMatch ? cal.getUnits() : cal.getYUnit(); s = (double) Math.round(roi.height*cal.pixelHeight*10)/10+" "+xunits;
w = ip2.getFontMetrics().stringWidth(s);
drawAxis(ip2, (int) p1x, (int) p1y, (int) p2x, (int) p2y, s, 10, -1, 1, 1);
String yunits = unitsMatch ? cal.getUnits() : cal.getXUnit(); s = (double) Math.round(roi.width*cal.pixelWidth*10)/10+" "+yunits;
w = ip2.getFontMetrics().stringWidth(s);
drawAxis(ip2, (int) p2x, (int) p2y, (int) p3x, (int) p3y, s, 10, 1, -1, 1);
}
void drawAxis(ImageProcessor ip, int x1, int y1, int x2, int y2, String label, int offset, int offsetXDirection, int offsetYDirection, int labelSide){
if(blackFill)
ip.setColor(Color.white);
else
ip.setColor(Color.black);
double m = -(double) (y2-y1)/(double) (x2-x1);
if(m==0)
m=.0001;
double mTangent = -1/m;
double theta = Math.atan(mTangent);
int dy = -offsetXDirection * (int) ( 7*Math.sin(theta) );
int dx = -offsetXDirection * (int) ( 7*Math.cos(theta) );
x1 += offsetXDirection * (int) ( offset*Math.cos(theta) );
x2 += offsetXDirection * (int) ( offset*Math.cos(theta) );
y1 += offsetYDirection * (int) ( offset*Math.sin(theta) );
y2 += offsetYDirection * (int) ( offset*Math.sin(theta) );
ip.drawLine(x1, y1, x2, y2);
ip.drawLine(x1, y1, x1+dx, y1-dy);
ip.drawLine(x2, y2, x2+dx, y2-dy);
ImageProcessor ipText = drawString( ip, label, (int) (Math.atan(m)/2/Math.PI*360) );
if(blackFill)
ipText.invert();
Blitter b;
if (ip instanceof ByteProcessor)
b = new ByteBlitter((ByteProcessor) ip);
else
b = new ColorBlitter((ColorProcessor) ip);
Color c = blackFill?Color.black:Color.white;
b.setTransparentColor(c);
int xloc = (x1+x2)/2-ipText.getWidth()/2 + offsetXDirection*labelSide*(int)(15*Math.cos(theta));
int yloc = (y1+y2)/2-ipText.getHeight()/2 + offsetYDirection*labelSide*(int)(15*Math.sin(theta));
b.copyBits(ipText, xloc, yloc, Blitter.COPY_TRANSPARENT);
return;
}
ImageProcessor drawString(ImageProcessor ip, String s, int a){
int w = ip.getFontMetrics().stringWidth(s);
int h = ip.getFontMetrics().getHeight();
int ipW, ipH;
double r = Math.sqrt( (w/2)*(w/2) + (h/2)*(h/2) );
double aR = (a/360.0)*2.0*Math.PI;
double aBaseR = Math.acos( (w/2)/r );
ipW = (int) Math.abs(r*Math.cos(aBaseR+aR));
ipH = (int) Math.abs(r*Math.sin(aBaseR+aR));
if((int) Math.abs(r*Math.cos(-aBaseR+aR))>ipW)
ipW = (int) Math.abs(r*Math.cos(-aBaseR+aR));
if((int) Math.abs(r*Math.sin(-aBaseR+aR))>ipH)
ipH = (int) Math.abs(r*Math.sin(-aBaseR+aR));
ipW *= 2;
ipH *= 2;
int tW = w;
if(ipW>w)
tW = ipW;
ImageProcessor ipText = new ByteProcessor(tW, ipH);
ipText.setFont(new Font("SansSerif", Font.PLAIN, fontSize));
ipText.setColor(Color.white);
ipText.fill();
ipText.setColor(Color.black);
ipText.setAntialiasedText(true);
ipText.drawString(s, tW/2-w/2, ipH/2+h/2);
ipText.setInterpolate(true);
ipText.rotate(-a);
ipText.setRoi(tW/2-ipW/2, 0, ipW, ipH);
ipText = ipText.crop();
return ipText;
}
void clearAboveProfile(ImageProcessor ipProfile, double[] profile, int width, double yinc2) {
byte[] pixels = (byte[])ipProfile.getPixels();
double ydelta = 0.0;
int height = ipProfile.getHeight();
for(int x=0; x<width; x++) {
ydelta += yinc2;
int top = height - (int)(profile[x]+ydelta);
for (int y=0,index=x; y<top; y++, index+=width)
pixels[index] = (byte)255;
}
}
ImageProcessor trimPlot(ImageProcessor plot, int maxTrim) {
int background = plot.getPixel(0, 0);
int width = plot.getWidth();
int height = plot.getHeight();
int trim = maxTrim-5;
a: for (int y=0; y<(maxTrim-5); y++)
for (int x=0; x<width; x++)
if (plot.getPixel(x,y)!=background)
{trim = y-5; break a;}
if (trim>10) {
plot.setRoi(0, trim, width, height-trim);
plot = plot.crop();
}
return plot;
}
void fixLut(ImageProcessor ip) {
if(!lut.isGrayscale() && lut.getMapSize() == 256){
for(int y=0;y<ip.getHeight();y++){
for(int x=0;x<ip.getWidth();x++){
if(ip.getPixelValue(x, y)==0){
ip.putPixelValue(x, y, 1);
}else if(ip.getPixelValue(x, y)==255){
ip.putPixelValue(x, y, 254);
}
}
}
byte[] rLUT = lut.getReds(); byte[] gLUT = lut.getGreens(); byte[] bLUT = lut.getBlues();
rLUT[0] = (byte)0;
gLUT[0] = (byte)0;
bLUT[0] = (byte)0;
rLUT[255] = (byte)255;
gLUT[255] = (byte)255;
bLUT[255] = (byte)255;
ip.setColorModel(new IndexColorModel(8, 256, rLUT, gLUT, bLUT));
}
}
}