package cz.cuni.lf1.lge.ThunderSTORM.rendering;
import cz.cuni.lf1.lge.ThunderSTORM.CameraSetupPlugIn;
import static cz.cuni.lf1.lge.ThunderSTORM.estimators.PSF.MoleculeDescriptor.Fitting.LABEL_UNCERTAINTY_Z;
import static cz.cuni.lf1.lge.ThunderSTORM.estimators.PSF.MoleculeDescriptor.Units.NANOMETER;
import static cz.cuni.lf1.lge.ThunderSTORM.estimators.PSF.MoleculeDescriptor.Units.PIXEL;
import cz.cuni.lf1.lge.ThunderSTORM.estimators.PSF.Molecule;
import cz.cuni.lf1.lge.ThunderSTORM.estimators.PSF.MoleculeDescriptor;
import static cz.cuni.lf1.lge.ThunderSTORM.estimators.PSF.MoleculeDescriptor.Fitting.LABEL_UNCERTAINTY_XY;
import ij.CompositeImage;
import ij.ImagePlus;
import ij.ImageStack;
import ij.measure.Calibration;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;
import ij.process.LUT;
import java.awt.Color;
import java.util.Arrays;
import java.util.List;
/**
* A common abstract superclass implementing RenderingMethod and
* IncrementalRenderingMethod. You can override the single abstract method
* drawPoint to quickly add another Rendering Method.
*/
public abstract class AbstractRendering implements RenderingMethod, IncrementalRenderingMethod {
protected double xmin, xmax, ymin, ymax;
protected double resolution;
protected int imSizeX, imSizeY;
protected double defaultDX;
protected double defaultDZ;
protected boolean forceDefaultDX;
protected boolean forceDefaultDZ;
protected double zFrom, zTo, zStep;
protected int zSlices;
protected boolean threeDimensions;
protected boolean colorize;
protected LUT colorizationLut;
protected ImageProcessor[] slices;
protected ImagePlus image;
private ImageStack stack;
/**
* A class for creating objects of sublasses of AbstractRendering.
*/
protected static abstract class AbstractBuilder<BuilderType extends AbstractBuilder, BuiltType extends AbstractRendering> {
protected double resolution, xmin = 0, xmax, ymin = 0, ymax;
protected int imSizeY, imSizeX;
protected boolean resolutionWasSet = false, roiWasSet = false, sizeWasSet = false;
protected final static double defaultResolution = 20;
private double defaultDX = 0.2;
private boolean forceDefaultDX = true;
private boolean forceDefaultDZ = true;
private double defaultDZ = 5;
private double zFrom = Double.NEGATIVE_INFINITY, zTo = Double.POSITIVE_INFINITY, zStep = Double.POSITIVE_INFINITY;
private int zSlices = 1;
private boolean threeDimensions = false;
private boolean colorize = false;
private LUT colorizationLut = null;
/**
* Sets the desired resolution of the final image. In localization units
* per pixel of the original image. <br> If the molecule locations are
* in nm, value of 20 signifies that one pixel in the superresolution
* image will be 20nm big <br> If the molecule locations are in pixels,
* a value of 0.2 signifies that the pixel size of the superresoultion
* image will be 0.2 times the pixel size of the original image.(5x
* smaller)
*
*/
public BuilderType resolution(double nmPerPixel) {
if(nmPerPixel <= 0) {
throw new IllegalArgumentException("Resolution must be positive. Passed value = " + nmPerPixel);
}
this.resolution = nmPerPixel;
resolutionWasSet = true;
return (BuilderType) this;
}
/**
* Sets the region of interest. Only molecules inside the roi will be
* rendered.
*
*/
public BuilderType roi(double xmin, double xmax, double ymin, double ymax) {
if(xmax < xmin || ymax < ymin) {
throw new IllegalArgumentException("xmax (ymax) must be greater than xmin (ymin)");
}
this.xmin = xmin;
this.xmax = xmax;
this.ymin = ymin;
this.ymax = ymax;
roiWasSet = true;
return (BuilderType) this;
}
/**
* Sets the size of the superresolution image
*/
public BuilderType imageSize(int x, int y) {
if(x <= 0 || y <= 0) {
throw new IllegalArgumentException("Image size must be positive. Passed values = " + x + " " + y);
}
this.imSizeX = x;
this.imSizeY = y;
sizeWasSet = true;
return (BuilderType) this;
}
public BuilderType defaultDX(double defaultDX) {
if(defaultDX <= 0) {
throw new IllegalArgumentException("Default dx must be positive. Passed value = " + defaultDX);
}
this.defaultDX = defaultDX;
resolutionWasSet = true;
return (BuilderType) this;
}
public BuilderType defaultDZ(double defaultDZ) {
if(defaultDZ <= 0) {
throw new IllegalArgumentException("Default dz must be positive. Passed value = " + defaultDZ);
}
this.defaultDZ = defaultDZ;
return (BuilderType) this;
}
/**
* Specifies whether the defaultDX value is used even if a dx (lateral
* uncertainty) value is provided for each molecule
*/
public BuilderType forceDefaultDX(boolean bool) {
this.forceDefaultDX = bool;
return (BuilderType) this;
}
public BuilderType forceDefaultDZ(boolean bool) {
this.forceDefaultDZ = bool;
return (BuilderType) this;
}
public BuilderType zRange(double from, double to, double step) {
if(to <= from) {
throw new IllegalArgumentException("Z range \"from\" value (" + from + ") must be smaller than \"to\" value (" + to + ").");
}
if(step <= 0) {
throw new IllegalArgumentException("Z range \"step\" value must be positive. Passed value = " + step);
}
this.zFrom = from;
this.zStep = step;
this.zSlices = (int) ((to - from) / step);
if(zSlices < 1) {
throw new RuntimeException("Invalid z range: Must have at least one slice.");
}
this.zTo = zSlices * step + from;
threeDimensions = true;
return (BuilderType) this;
}
public BuilderType colorize(boolean colorize) {
this.colorize = colorize;
return (BuilderType) this;
}
public BuilderType colorizationLUT(LUT lut) {
this.colorizationLut = lut;
return (BuilderType) this;
}
protected void validate() {
if(!roiWasSet && !sizeWasSet) {
throw new IllegalArgumentException("Image size must be resolved while building. Set at least image size or roi.");
}
if(!roiWasSet && sizeWasSet) {
if(!resolutionWasSet) {
resolution = defaultResolution;
}
xmax = imSizeX * resolution;
ymax = imSizeY * resolution;
xmin = 0;
ymin = 0;
} else if(roiWasSet && !sizeWasSet) {
if(!resolutionWasSet) {
resolution = defaultResolution;
}
imSizeX = (int)((xmax - xmin) / resolution);
imSizeY = (int)((ymax - ymin) / resolution);
} else {
if(resolutionWasSet) {
int newImSizeX = (int)((xmax - xmin) / resolution);
int newImSizeY = (int)((ymax - ymin) / resolution);
if(newImSizeX != imSizeX || newImSizeY != imSizeY) {
throw new IllegalArgumentException("Invalid combination of image size, roi and resolution. Set only two of them.");
}
} else {
resolution = (xmax - xmin) / imSizeX;
if(Math.abs(resolution - (ymax - ymin) / imSizeY) > 0.001) {
throw new IllegalArgumentException("Resolution in x and y appears to be different.");
}
}
}
long pixelcount = (long) imSizeX * (long) imSizeY;
if(pixelcount > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Tried to create too big image (" + imSizeX + " x " + imSizeY + "). Check that parameters are correct and use appropriate units.");
}
}
/**
* Returns the newly created object.
*/
public abstract BuiltType build();
}
protected AbstractRendering(AbstractBuilder builder) {
this.xmin = builder.xmin;
this.xmax = builder.xmax;
this.ymin = builder.ymin;
this.ymax = builder.ymax;
this.resolution = builder.resolution;
this.imSizeX = builder.imSizeX;
this.imSizeY = builder.imSizeY;
this.forceDefaultDX = builder.forceDefaultDX;
this.forceDefaultDZ = builder.forceDefaultDZ;
this.defaultDX = builder.defaultDX;
this.defaultDZ = builder.defaultDZ;
this.zFrom = builder.zFrom;
this.zSlices = builder.zSlices;
this.zStep = builder.zStep;
this.zTo = builder.zTo;
this.threeDimensions = builder.threeDimensions;
this.colorize = builder.colorize;
this.colorizationLut = builder.colorizationLut;
slices = new ImageProcessor[zSlices];
stack = new ImageStack(imSizeX, imSizeY);
for(int i = 0; i < zSlices; i++) {
slices[i] = new FloatProcessor(imSizeX, imSizeY);
stack.addSlice((i * zStep + zFrom) + " to " + ((i + 1) * zStep + zFrom), slices[i]);
}
image = new ImagePlus(getRendererName(), stack);
Calibration calibration = new Calibration();
double pixelSize = resolution * CameraSetupPlugIn.getPixelSize() / 1000;
calibration.pixelHeight = pixelSize;
calibration.pixelWidth = pixelSize;
if(threeDimensions) {
calibration.pixelDepth = zStep / 1000;
}
calibration.setUnit("um");
image.setCalibration(calibration);
if(colorize) {
image.setDimensions(zSlices, 1, 1);
CompositeImage image2 = new CompositeImage(image);
image = image2;
setupLuts();
}
}
@Override
public void addToImage(double[] x, double[] y, double[] z, double[] dx, double[] dz) {
for(int i = 0; i < x.length; i++) {
double zVal = z != null ? z[i] : 0;
double dxVal = dx != null && !forceDefaultDX ? dx[i] : defaultDX;
double dzVal = dz != null && !forceDefaultDZ ? dz[i] : defaultDZ;
drawPoint(x[i], y[i], zVal, dxVal, dzVal);
}
}
@Override
public void addToImage(List<Molecule> fits) {
if(fits.isEmpty()) {
return;
}
MoleculeDescriptor descriptor = fits.get(0).descriptor;
boolean useDefaultDX = forceDefaultDX || !descriptor.hasParam(LABEL_UNCERTAINTY_XY);
boolean useDefaultDZ = forceDefaultDZ || !descriptor.hasParam(LABEL_UNCERTAINTY_Z);
for(int i = 0, im = fits.size(); i < im; i++) {
Molecule fit = fits.get(i);
double zVal = fit.getZ();
double dxVal = useDefaultDX ? defaultDX : fit.getParam(LABEL_UNCERTAINTY_XY, PIXEL);
double dzVal = useDefaultDZ ? defaultDZ : fit.getParam(LABEL_UNCERTAINTY_Z, NANOMETER);
//
drawPoint(fit.getX(PIXEL), fit.getY(PIXEL), zVal, dxVal, dzVal);
}
}
@Override
public ImagePlus getRenderedImage() {
return image;
}
@Override
public ImagePlus getRenderedImage(double[] x, double[] y, double[] z, double[] dx, double[] dz) {
reset();
addToImage(x, y, z, dx, dz);
return getRenderedImage();
}
protected abstract void drawPoint(double x, double y, double z, double dx, double dz);
@Override
public void reset() {
for(int i = 0; i < slices.length; i++) {
float[] px = (float[]) slices[i].getPixels();
Arrays.fill(px, 0);
}
stack = new ImageStack(imSizeX, imSizeY);
for(int i = 0; i < zSlices; i++) {
stack.addSlice((i * zStep + zFrom) + " to " + ((i + 1) * zStep + zFrom), slices[i]);
}
image.setStack(stack);
setupLuts();
}
protected boolean isInBounds(double x, double y) {
return x >= xmin && x < xmax && y >= ymin && y < ymax && !Double.isNaN(x) && !Double.isNaN(y);
}
private void setupLuts() {
if(image.isComposite()) {
CompositeImage image2 = (CompositeImage) image;
LUT[] channeLuts = new LUT[zSlices];
if (colorizationLut == null) { // fallback if no LUTs are installed
for (int i = 0; i < channeLuts.length; i++) {
//Colormap for slices: (has constant grayscale intensity, unlike jet and similar)
//r: /
// __/
//g: /\
// / \
//b: \
// \__
float norm = (float) i / zSlices;
float r, g, b;
if (norm < 0.5) {
b = 1 - 2 * norm;
g = 2 * norm;
r = 0;
} else {
b = 0;
g = -2 * norm + 2;
r = 2 * norm - 1;
}
channeLuts[i] = LUT.createLutFromColor(new Color(r, g, b));
}
} else {
int[] rgb = new int[4];
for (int i = 0; i < channeLuts.length; i++) {
colorizationLut.getComponents((int)(((float) i / zSlices) * 255f), rgb, 0);
channeLuts[i] = LUT.createLutFromColor(new Color(rgb[0], rgb[1], rgb[2]));
}
}
image2.setLuts(channeLuts);
}
}
}