diff --git a/src/main/java/algorithms/Histogram2D.java b/src/main/java/algorithms/Histogram2D.java index 9a5a04b..0297e11 100644 --- a/src/main/java/algorithms/Histogram2D.java +++ b/src/main/java/algorithms/Histogram2D.java @@ -1,380 +1,393 @@ package algorithms; import gadgets.DataContainer; import ij.measure.ResultsTable; import java.util.EnumSet; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessibleInterval; import net.imglib2.TwinCursor; import net.imglib2.img.ImgFactory; import net.imglib2.img.array.ArrayImgFactory; import net.imglib2.type.logic.BitType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.LongType; import net.imglib2.view.Views; import results.ResultHandler; /** * Represents the creation of a 2D histogram between two images. * Channel 1 is set out in x direction, while channel 2 in y direction. * @param The source images value type */ public class Histogram2D> extends Algorithm { // An enumeration of possible drawings public enum DrawingFlags { Plot, RegressionLine, Axes } // the drawing configuration EnumSet drawingSettings; // The width of the scatter-plot protected int xBins = 256; // The height of the scatter-plot protected int yBins = 256; // The name of the result 2D histogram to pass elsewhere protected String title = ""; // Swap or not swap ch1 and ch2 protected boolean swapChannels = false; // member variables for labeling protected String ch1Label = "Channel 1"; protected String ch2Label = "Channel 2"; // Result keeping members // the generated plot image private RandomAccessibleInterval plotImage; // the bin widths for each channel private double xBinWidth = 0.0, yBinWidth = 0.0; // labels for the axes private String xLabel = "", yLabel = ""; // ranges for the axes private double xMin = 0.0, xMax = 0.0, yMin = 0.0, yMax = 0.0; public Histogram2D(){ this("2D Histogram"); } public Histogram2D(String title){ this(title, false); } public Histogram2D(String title, boolean swapChannels){ this(title, swapChannels, EnumSet.of( DrawingFlags.Plot, DrawingFlags.RegressionLine )); } public Histogram2D(String title, boolean swapChannels, EnumSet drawingSettings){ super(title); this.title = title; this.swapChannels = swapChannels; if (swapChannels) { int xBins = this.xBins; this.xBins = this.yBins; this.yBins = xBins; } this.drawingSettings = drawingSettings; } /** * Gets the minimum of channel one. Takes channel * swapping into consideration and will return min * of channel two if swapped. * * @return The minimum of what is seen as channel one. */ protected double getMinCh1(DataContainer container) { return swapChannels ? container.getMinCh2() : container.getMinCh1(); } /** * Gets the minimum of channel two. Takes channel * swapping into consideration and will return min * of channel one if swapped. * * @return The minimum of what is seen as channel two. */ protected double getMinCh2(DataContainer container) { return swapChannels ? container.getMinCh1() : container.getMinCh2(); } /** * Gets the maximum of channel one. Takes channel * swapping into consideration and will return max * of channel two if swapped. * * @return The maximum of what is seen as channel one. */ protected double getMaxCh1(DataContainer container) { return swapChannels ? container.getMaxCh2() : container.getMaxCh1(); } /** * Gets the maximum of channel two. Takes channel * swapping into consideration and will return max * of channel one if swapped. * * @return The maximum of what is seen as channel two. */ protected double getMaxCh2(DataContainer container) { return swapChannels ? container.getMaxCh1() : container.getMaxCh2(); } /** * Gets the image of channel one. Takes channel * swapping into consideration and will return image * of channel two if swapped. * * @return The image of what is seen as channel one. */ protected RandomAccessibleInterval getImageCh1(DataContainer container) { return swapChannels ? container.getSourceImage2() : container.getSourceImage1(); } /** * Gets the image of channel two. Takes channel * swapping into consideration and will return image * of channel one if swapped. * * @return The image of what is seen as channel two. */ protected RandomAccessibleInterval getImageCh2(DataContainer container) { return swapChannels ? container.getSourceImage1() : container.getSourceImage2(); } /** * Gets the label of channel one. Takes channel * swapping into consideration and will return label * of channel two if swapped. * * @return The label of what is seen as channel one. */ protected String getLabelCh1() { return swapChannels ? ch2Label : ch1Label; } /** * Gets the label of channel two. Takes channel * swapping into consideration and will return label * of channel one if swapped. * * @return The label of what is seen as channel two. */ protected String getLabelCh2() { return swapChannels ? ch1Label : ch2Label; } @Override public void execute(DataContainer container) throws MissingPreconditionException { generateHistogramData(container); } protected void generateHistogramData(DataContainer container) { double ch1BinWidth = getXBinWidth(container); double ch2BinWidth = getYBinWidth(container); // get the 2 images for the calculation of Pearson's final RandomAccessibleInterval img1 = getImageCh1(container); final RandomAccessibleInterval img2 = getImageCh2(container); final RandomAccessibleInterval mask = container.getMask(); // get the cursors for iterating through pixels in images TwinCursor cursor = new TwinCursor(img1.randomAccess(), img2.randomAccess(), Views.iterable(mask).localizingCursor()); // create new image to put the scatter-plot in final ImgFactory scatterFactory = new ArrayImgFactory< LongType >(); plotImage = scatterFactory.create(new int[] {xBins, yBins}, new LongType() ); // create access cursors final RandomAccess histogram2DCursor = plotImage.randomAccess(); + long ignoredPixelCount = 0; + // iterate over images long[] pos = new long[ plotImage.numDimensions() ]; while (cursor.hasNext()) { cursor.fwd(); double ch1 = cursor.getFirst().getRealDouble(); double ch2 = cursor.getSecond().getRealDouble(); /* Scale values for both channels to fit in the range. * Moreover mirror the y value on the x axis. */ pos[0] = getXValue(ch1, ch1BinWidth, ch2, ch2BinWidth); pos[1] = getYValue(ch1, ch1BinWidth, ch2, ch2BinWidth); - // set position of input/output cursor - histogram2DCursor.setPosition( pos ); - // get current value at position and increment it - long count = histogram2DCursor.get().getIntegerLong(); - count++; - histogram2DCursor.get().set(count); + if (pos[0] >= 0 && pos[1] >=0 && pos[0] < xBins && pos[1] < yBins) { + // set position of input/output cursor + histogram2DCursor.setPosition( pos ); + // get current value at position and increment it + long count = histogram2DCursor.get().getIntegerLong(); + count++; + + histogram2DCursor.get().set(count); + } else { + ignoredPixelCount ++; + } } + if (ignoredPixelCount > 0) { + addWarning("Ignored pixels while generating histogram.", + "" + ignoredPixelCount + " pixels were ignored while generating the 2D histogram \"" + title + + "\" because the grey values were out of range." + + "This may happen, if an image contains negative pixel values."); + } xBinWidth = ch1BinWidth; yBinWidth = ch2BinWidth; xLabel = getLabelCh1(); yLabel = getLabelCh2(); xMin = getXMin(container); xMax = getXMax(container); yMin = getYMin(container); yMax = getYMax(container); } /** * A table of x-values, y-values and the counts is generated and * returned as a string. The single fields in one row (X Y Count) * are separated by tabs. * * @return A String representation of the histogram data. */ public String getData() { StringBuffer sb = new StringBuffer(); double xBinWidth = 1.0 / getXBinWidth(); double yBinWidth = 1.0 / getYBinWidth(); double xMin = getXMin(); double yMin = getYMin(); // check if we have bins of size one or other ones boolean xBinWidthIsOne = Math.abs(xBinWidth - 1.0) < 0.00001; boolean yBinWidthIsOne = Math.abs(yBinWidth - 1.0) < 0.00001; // configure decimal places accordingly int xDecimalPlaces = xBinWidthIsOne ? 0 : 3; int yDecimalPlaces = yBinWidthIsOne ? 0 : 3; // create a cursor to access the histogram data RandomAccess cursor = plotImage.randomAccess(); // loop over 2D histogram for (int i=0; i < plotImage.dimension(0); ++i) { for (int j=0; j < plotImage.dimension(1); ++j) { cursor.setPosition(i, 0); cursor.setPosition(j, 1); sb.append( ResultsTable.d2s(xMin + (i * xBinWidth), xDecimalPlaces) + "\t" + ResultsTable.d2s(yMin + (j * yBinWidth), yDecimalPlaces) + "\t" + ResultsTable.d2s(cursor.get().getRealDouble(), 0) + "\n"); } } return sb.toString(); } @Override public void processResults(ResultHandler handler) { super.processResults(handler); handler.handleHistogram( this, title ); } /** * Calculates the bin width of one bin in x/ch1 direction. * @param container The container with images to work on * @return The width of one bin in x direction */ protected double getXBinWidth(DataContainer container) { double ch1Max = getMaxCh1(container); if (ch1Max < yBins) { // bin widths must not exceed 1 return 1; } // we need (ch1Max * width + 0.5) < yBins, but just so, i.e. // ch1Max * width + 0.5 == yBins - eps // width = (yBins - 0.5 - eps) / ch1Max return (yBins - 0.50001) / ch1Max; } /** * Calculates the bin width of one bin in y/ch2 direction. * @param container The container with images to work on * @return The width of one bin in y direction */ protected double getYBinWidth(DataContainer container) { double ch2Max = getMaxCh2(container); if (ch2Max < yBins) { // bin widths must not exceed 1 return 1; } return (yBins - 0.50001) / ch2Max; } /** * Calculates the locations x value. * @param ch1Val The intensity of channel one * @param ch1BinWidt The bin width for channel one * @return The x value of the data point location */ protected int getXValue(double ch1Val, double ch1BinWidth, double ch2Val, double ch2BinWidth) { return (int)(ch1Val * ch1BinWidth + 0.5); } /** * Calculates the locations y value. * @param ch2Val The intensity of channel one * @param ch2BinWidt The bin width for channel one * @return The x value of the data point location */ protected int getYValue(double ch1Val, double ch1BinWidth, double ch2Val, double ch2BinWidth) { return (yBins - 1) - (int)(ch2Val * ch2BinWidth + 0.5); } protected double getXMin(DataContainer container) { return 0; } protected double getXMax(DataContainer container) { return swapChannels ? getMaxCh2(container) : getMaxCh1(container); } protected double getYMin(DataContainer container) { return 0; } protected double getYMax(DataContainer container) { return swapChannels ? getMaxCh1(container) : getMaxCh2(container); } // Result access methods public RandomAccessibleInterval getPlotImage() { return plotImage; } public double getXBinWidth() { return xBinWidth; } public double getYBinWidth() { return yBinWidth; } public String getXLabel() { return xLabel; } public String getYLabel() { return yLabel; } public double getXMin() { return xMin; } public double getXMax() { return xMax; } public double getYMin() { return yMin; } public double getYMax() { return yMax; } public String getTitle() { return title; } public EnumSet getDrawingSettings() { return drawingSettings; } } diff --git a/src/main/java/algorithms/InputCheck.java b/src/main/java/algorithms/InputCheck.java index cb05bf8..05240bd 100644 --- a/src/main/java/algorithms/InputCheck.java +++ b/src/main/java/algorithms/InputCheck.java @@ -1,178 +1,184 @@ package algorithms; import gadgets.DataContainer; import gadgets.DataContainer.MaskType; import ij.IJ; import net.imglib2.RandomAccessibleInterval; import net.imglib2.TwinCursor; import net.imglib2.type.logic.BitType; import net.imglib2.type.numeric.RealType; import net.imglib2.view.Views; import results.ResultHandler; /** * This class implements some basic checks for the input image data. For * instance: Is the percentage of zero-zero or saturated pixels too high? Also, * we get basic image properties / stats from imglib2, and also the * colocalization job name from the DataContainer and allow implementations of * ResultHandler to report them. */ public class InputCheck> extends Algorithm { /* the maximum allowed ratio between zero-zero and * normal pixels */ protected final double maxZeroZeroRatio = 0.1f; /* the maximum allowed ratio between saturated and * normal pixels within a channel */ protected final double maxSaturatedRatio = 0.1f; // the zero-zero pixel ratio double zeroZeroPixelRatio; // the saturated pixel ratio of channel 1 double saturatedRatioCh1; // the saturated pixel ratio of channel 2 double saturatedRatioCh2; // the coloc job name String colocJobName; // general image stats/parameters/values double ch1Max; double ch2Max; double ch1Min; double ch2Min; double ch1Mean; double ch2Mean; double ch1Integral; double ch2Integral; // Mask infos MaskType maskType; double maskID; public InputCheck() { super("input data check"); } @Override public void execute(DataContainer container) throws MissingPreconditionException { // get the 2 images and the mask final RandomAccessibleInterval img1 = container.getSourceImage1(); final RandomAccessibleInterval img2 = container.getSourceImage2(); final RandomAccessibleInterval mask = container.getMask(); // get the cursors for iterating through pixels in images TwinCursor cursor = new TwinCursor(img1.randomAccess(), img2.randomAccess(), Views.iterable(mask).cursor()); // get various general image properties/stats/values from the DataContainer ch1Max = container.getMaxCh1(); ch2Max = container.getMaxCh2(); ch1Min = container.getMinCh1(); ch2Min = container.getMinCh2(); ch1Mean = container.getMeanCh1(); ch2Mean = container.getMeanCh2(); ch1Integral = container.getIntegralCh1(); ch2Integral = container.getIntegralCh2(); // get the info about the mask/ROI being used or not. maskType = container.getMaskType(); maskID = (double)container.getMaskID(); // the total amount of pixels that have been taken into consideration long N = 0; // the number of pixels that are zero in both channels long Nzero = 0; // the number of ch1 pixels with the maximum ch1 value; long NsaturatedCh1 = 0; // the number of ch2 pixels with the maximum ch2 value; long NsaturatedCh2 = 0; while (cursor.hasNext()) { cursor.fwd(); double ch1 = cursor.getFirst().getRealDouble(); double ch2 = cursor.getSecond().getRealDouble(); // is the current pixels combination a zero-zero pixel? if (Math.abs(ch1 + ch2) < 0.00001) Nzero++; // is the current pixel of channel one saturated? if (Math.abs(ch1Max - ch1) < 0.00001) NsaturatedCh1++; // is the current pixel of channel one saturated? if (Math.abs(ch2Max - ch2) < 0.00001) NsaturatedCh2++; N++; } // calculate results double zeroZeroRatio = (double)Nzero / (double)N; // for channel wise ratios we have to use half of the total pixel amount double ch1SaturatedRatio = (double)NsaturatedCh1 / ( (double)N *0.5); double ch2SaturatedRatio = (double)NsaturatedCh2 / ( (double)N * 0.5); /* save results * Percentage results need to be multiplied by 100 */ zeroZeroPixelRatio = zeroZeroRatio * 100.0; saturatedRatioCh1 = ch1SaturatedRatio * 100.0; saturatedRatioCh2 = ch2SaturatedRatio * 100.0; // get job name so the ResultsHandler implementation can have it. colocJobName = container.getJobName(); + // add warnings if images contain negative values + if (ch1Min < 0 || ch2Min < 0) { + addWarning("Negative minimum pixel value found.", + "The minimum pixel value in at least one of the channels is negative. Negative values might break the logic of some analysis methods by breaking a basic basic assumption: The pixel value is assumed to be proportional to the number of photons detected in a pixel. Negative photon counts make no physical sense. Set negative pixel values to zero, or shift pixel intensities higher so there are no negative pixel values."); + } + // add warnings if values are not in tolerance range if ( Math.abs(zeroZeroRatio) > maxZeroZeroRatio ) { - addWarning("zero-zero ratio too high", - "The ratio between zero-zero pixels and other pixels is larger " + addWarning("Zero-zero ratio too high", + "The ratio between zero-zero pixels and other pixels is large: " + IJ.d2s(zeroZeroRatio, 2) + ". Maybe you should use a ROI."); } if ( Math.abs(ch1SaturatedRatio) > maxSaturatedRatio ) { - addWarning("saturated ch1 ratio too high", - "The ratio between saturated pixels and other pixels in channel one is larger " + addWarning("Saturated ch1 ratio too high", + "The ratio between saturated pixels and other pixels in channel one is large: " + IJ.d2s(maxSaturatedRatio, 2) + ". Maybe you should use a ROI."); } if ( Math.abs(ch1SaturatedRatio) > maxSaturatedRatio ) { - addWarning("saturated ch2 ratio too high", - "The ratio between saturated pixels and other pixels in channel two is larger " + addWarning("Saturated ch2 ratio too high", + "The ratio between saturated pixels and other pixels in channel two is large: " + IJ.d2s(maxSaturatedRatio, 2) + ". Maybe you should use a ROI."); } } @Override public void processResults(ResultHandler handler) { super.processResults(handler); // Let us have a ValueResult for the colocalisation analysis job name: // A ValueResult can be two Strings (or a string and a numerical value) // We want to keep the jobName close to all the value results // so they get shown together by whatever implementation of ResultsHandler. handler.handleValue("Coloc_Job_Name", colocJobName); // Make the ResultsHander implementation deal with the input check results. handler.handleValue("% zero-zero pixels", zeroZeroPixelRatio, 2); handler.handleValue("% saturated ch1 pixels", saturatedRatioCh1, 2); handler.handleValue("% saturated ch2 pixels", saturatedRatioCh2, 2); // Make the ResultsHander implementation deal with the images' // stats/parameters/values handler.handleValue("Channel 1 Max", ch1Max, 3); handler.handleValue("Channel 2 Max", ch2Max, 3); handler.handleValue("Channel 1 Min", ch1Min, 3); handler.handleValue("Channel 2 Min", ch2Min, 3); handler.handleValue("Channel 1 Mean", ch1Mean, 3); handler.handleValue("Channel 2 Mean", ch2Mean, 3); handler.handleValue("Channel 1 Integrated (Sum) Intensity", ch1Integral, 3); handler.handleValue("Channel 2 Integrated (Sum) Intensity", ch2Integral, 3); // Make the ResultsHandler implementation deal with the images' // ROI or mask or lack thereof, so the user knows what they used. handler.handleValue("Mask Type Used", maskType.name()); handler.handleValue("Mask ID Used", maskID, 0); } }