diff --git a/src/main/java/algorithms/MandersColocalization.java b/src/main/java/algorithms/MandersColocalization.java index 85dc49c..1017cbf 100644 --- a/src/main/java/algorithms/MandersColocalization.java +++ b/src/main/java/algorithms/MandersColocalization.java @@ -1,251 +1,170 @@ package algorithms; import gadgets.DataContainer; import gadgets.ThresholdMode; import net.imglib2.RandomAccessible; 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 algorithm calculates Manders et al.'s split colocalization - * coefficients, M1 and M2. These are independent of signal intensities, - * but are directly proportional to the amount of - * fluorescence in the colocalized objects in each colour channel of the - * image, relative to the total amount of fluorescence in that channel. - * See "Manders, Verbeek, Aten - Measurement of colocalization - * of objects in dual-colour confocal images. J. Microscopy, vol. 169 - * pt 3, March 1993, pp 375-382". - * - * M1 = sum of Channel 1 intensity in pixels over the channel 2 threshold / total Channel 1 intensity. - * M2 is vice versa. - * The result is a fraction (range 0-1, but often misrepresented as a %. We wont do that here. - * - * Further, it also calculates other split colocalization coefficients, - * such as fraction of pixels (voxels) colocalized, - * or fraction of intensity colocalized, as described at: - * http://www.uhnresearch.ca/facilities/wcif/imagej/colour_analysis.htm - * copy pasted here - credits to Tony Collins. - * - * Number of colocalised voxels – Ncoloc - * This is the number of voxels which have both channel 1 and channel 2 intensities above threshold - * (i.e., the number of pixels in the yellow area of the scatterplot). - * - * %Image volume colocalised – %Volume - * This is the percentage of voxels which have both channel 1 and channel 2 intensities above threshold, - * expressed as a percentage of the total number of pixels in the image (including zero-zero pixels); - * in other words, the number of pixels in the scatterplot’s yellow area ÷ total number of pixels in the scatter plot (the Red + Green + Blue + Yellow areas). - * - * %Voxels Colocalised – %Ch1 Vol; %Ch2 Vol - * This generates a value for each channel. This is the number of voxels for each channel - * which have both channel 1 and channel 2 intensities above threshold, - * expressed as a percentage of the total number of voxels for each channel - * above their respective thresholds; in other words, for channel 1 (along the x-axis), - * this equals the (the number of pixels in the Yellow area) ÷ (the number of pixels in the Blue + Yellow areas). - * For channel 2 this is calculated as follows: - * (the number of pixels in the Yellow area) ÷ (the number of pixels in the Red + Yellow areas). - * - * %Intensity Colocalised – %Ch1 Int; %Ch2 Int - * This generates a value for each channel. For channel 1, this value is equal to - * the sum of the pixel intensities, with intensities above both channel 1 and channel 2 thresholds - * expressed as a percentage of the sum of all channel 1 intensities; - * in other words, it is calculated as follows: - * (the sum of channel 1 pixel intensities in the Yellow area) ÷ (the sum of channel 1 pixels intensities in the Red + Green + Blue + Yellow areas). - - * %Intensities above threshold colocalised – %Ch1 Int > thresh; %Ch2 Int > thresh - * This generates a value for each channel. For channel 1, - * this value is equal to the sum of the pixel intensities - * with intensities above both channel 1 and channel 2 thresholds - * expressed as a percentage of the sum of all channel 1 intensities above the threshold for channel 1. - * In other words, it is calculated as follows: - * (the sum of channel 1 pixel intensities in the Yellow area) ÷ (sum of channel 1 pixels intensities in the Blue + Yellow area) - * - * The results are often represented as % values, but to make them consistent with Manders' - * split coefficients, we will also report them as fractions (range 0-1). + * This algorithm calculates Manders et al.'s two two correlation + * values M1 and M2. Those coefficients are independent from signal + * intensities, but are directly proportional to the amount of + * flourescence in the co-localized objects in each component of the + * image. See "Manders, Verbeek, Aten - Measurement of co-localization + * of objects in dual-colour confocal images". * * @param */ public class MandersColocalization> extends Algorithm { - // Manders M1 and M2 values - - // fraction of intensity of a channel, in pixels above zero in the other channel. + // Manders M1 and M2 value double mandersM1, mandersM2; - - // thresholded Manders M1 and M2 values, - // fraction of intensity of a channel, in pixels above threshold in the other channel. + // thresholded Manders M1 and M2 values double mandersThresholdedM1, mandersThresholdedM2; - // Number of colocalized voxels (pixels) – Ncoloc - long numberOfPixelsAboveBothThresholds; - - // Fraction of Image volume colocalized – Fraction of Volume - double fractionOfPixelsAboveBothThresholds; - - // Fraction Voxels (pixels) Colocalized – Fraction of Ch1 Vol; Fraction of Ch2 Vol - double fractionOfColocCh1Pixels, fractionOfColocCh2Pixels; - - // Fraction Intensity Colocalized – Fraction of Ch1 Int; Fraction of Ch2 Int - double fractionOfColocCh1Intensity, fractionOfColocCh2Intensity; - - // Fraction of Intensities above threshold, colocalized – - // Fraction of Ch1 Int > thresh; Fraction of Ch2 Int > thresh - double fractionOfColocCh1IntensityAboveCh1Thresh, fractionOfColocCh2IntensityAboveCh2Thresh; - /** * A result container for Manders' calculations. */ public static class MandersResults { public double m1; public double m2; } public MandersColocalization() { super("Manders correlation"); } @Override public void execute(DataContainer container) throws MissingPreconditionException { // get the two images for the calculation of Manders' values RandomAccessible img1 = container.getSourceImage1(); RandomAccessible img2 = container.getSourceImage2(); RandomAccessibleInterval mask = container.getMask(); TwinCursor cursor = new TwinCursor(img1.randomAccess(), img2.randomAccess(), Views.iterable(mask).localizingCursor()); - // calculate Manders' coefficients without threshold + // calculate Mander's values without threshold MandersResults results = calculateMandersCorrelation(cursor, img1.randomAccess().get().createVariable()); // save the results mandersM1 = results.m1; mandersM2 = results.m2; - // calculate the thresholded split Manders' coefficients, if possible + // calculate the thresholded values, if possible AutoThresholdRegression autoThreshold = container.getAutoThreshold(); if (autoThreshold != null ) { - // calculate thresholded Manders' coefficients + // calculate Mander's values cursor.reset(); results = calculateMandersCorrelation(cursor, autoThreshold.getCh1MaxThreshold(), autoThreshold.getCh2MaxThreshold(), ThresholdMode.Above); // save the results mandersThresholdedM1 = results.m1; mandersThresholdedM2 = results.m2; } } /** - * Calculates Manders' split M1 and M2 coefficients, without a threshold + * Calculates Manders' split M1 and M2 values without a threshold * * @param cursor A TwinCursor that walks over two images * @param type A type instance, its value is not relevant * @return Both Manders' M1 and M2 values */ public MandersResults calculateMandersCorrelation(TwinCursor cursor, T type) { return calculateMandersCorrelation(cursor, type, type, ThresholdMode.None); } - - /** - * Calculates Manders' split M1 and M2 coefficients, with a threshold - * - * @param cursor A TwinCursor that walks over two images - * @param type A type instance, its value is not relevant - * @param thresholdCh1 type T - * @param thresholdCh2 type T - * @param tmode A ThresholdMode the threshold mode - * @return Both Manders' M1 and M2 values - */ + public MandersResults calculateMandersCorrelation(TwinCursor cursor, final T thresholdCh1, final T thresholdCh2, ThresholdMode tMode) { - SplitCoeffAccumulator mandersAccum; + MandersAccumulator acc; // create a zero-values variable to compare to later on final T zero = thresholdCh1.createVariable(); zero.setZero(); - // iterate over images - set the boolean value for if a pixel is thresholded + // iterate over images if (tMode == ThresholdMode.None) { - mandersAccum = new SplitCoeffAccumulator(cursor) { - final boolean acceptMandersCh1(T type1, T type2) { + acc = new MandersAccumulator(cursor) { + final boolean accecptCh1(T type1, T type2) { return (type2.compareTo(zero) > 0); } - final boolean acceptMandersCh2(T type1, T type2) { + final boolean accecptCh2(T type1, T type2) { return (type1.compareTo(zero) > 0); } }; } else if (tMode == ThresholdMode.Below) { - mandersAccum = new SplitCoeffAccumulator(cursor) { - final boolean acceptMandersCh1(T type1, T type2) { + acc = new MandersAccumulator(cursor) { + final boolean accecptCh1(T type1, T type2) { return (type2.compareTo(zero) > 0) && - (type2.compareTo(thresholdCh2) <= 0); + (type1.compareTo(thresholdCh1) <= 0); } - final boolean acceptMandersCh2(T type1, T type2) { + final boolean accecptCh2(T type1, T type2) { return (type1.compareTo(zero) > 0) && - (type1.compareTo(thresholdCh1) <= 0); + (type2.compareTo(thresholdCh2) <= 0); } }; } else if (tMode == ThresholdMode.Above) { - mandersAccum = new SplitCoeffAccumulator(cursor) { - final boolean acceptMandersCh1(T type1, T type2) { + acc = new MandersAccumulator(cursor) { + final boolean accecptCh1(T type1, T type2) { return (type2.compareTo(zero) > 0) && - (type2.compareTo(thresholdCh2) >= 0); + (type1.compareTo(thresholdCh1) >= 0); } - final boolean acceptMandersCh2(T type1, T type2) { + final boolean accecptCh2(T type1, T type2) { return (type1.compareTo(zero) > 0) && - (type1.compareTo(thresholdCh1) >= 0); + (type2.compareTo(thresholdCh2) >= 0); } }; } else { throw new UnsupportedOperationException(); } MandersResults results = new MandersResults(); - // calculate the results, see description above. - results.m1 = mandersAccum.mandersSumCh1 / mandersAccum.sumCh1; - results.m2 = mandersAccum.mandersSumCh2 / mandersAccum.sumCh2; + // calculate the results + results.m1 = acc.condSumCh1 / acc.sumCh1; + results.m2 = acc.condSumCh2 / acc.sumCh2; return results; } @Override public void processResults(ResultHandler handler) { super.processResults(handler); - handler.handleValue( "Manders' M1 (no threshold)", mandersM1 ); - handler.handleValue( "Manders' M2 (no threshold)", mandersM2 ); - handler.handleValue( "Manders' M1 (threshold)", mandersThresholdedM1 ); - handler.handleValue( "Manders' M2 (threshold)", mandersThresholdedM2 ); + handler.handleValue( "Manders M1 (no threshold)", mandersM1 ); + handler.handleValue( "Manders M2 (no threshold)", mandersM2 ); + handler.handleValue( "Manders M1 (threshold)", mandersThresholdedM1 ); + handler.handleValue( "Manders M2 (threshold)", mandersThresholdedM2 ); } /** * A class similar to the Accumulator class, but more specific - * to the split Manders and other split channel calculations. + * to the Manders calculations. */ - protected abstract class SplitCoeffAccumulator { - double sumCh1, sumCh2, mandersSumCh1, mandersSumCh2; + protected abstract class MandersAccumulator { + double sumCh1, sumCh2, condSumCh1, condSumCh2; - public SplitCoeffAccumulator(TwinCursor cursor) { + public MandersAccumulator(TwinCursor cursor) { while (cursor.hasNext()) { cursor.fwd(); T type1 = cursor.getFirst(); T type2 = cursor.getSecond(); double ch1 = type1.getRealDouble(); double ch2 = type2.getRealDouble(); - - // boolean logics for adding or not adding to the different value counters for a pixel. - if (acceptMandersCh1(type1, type2)) - mandersSumCh1 += ch1; - if (acceptMandersCh2(type1, type2)) - mandersSumCh2 += ch2; - - // add this pixel's two intensity values to the ch1 and ch2 sum counters + if (accecptCh1(type1, type2)) + condSumCh1 += ch1; + if (accecptCh2(type1, type2)) + condSumCh2 += ch2; sumCh1 += ch1; sumCh2 += ch2; } } - abstract boolean acceptMandersCh1(T type1, T type2); - abstract boolean acceptMandersCh2(T type1, T type2); + abstract boolean accecptCh1(T type1, T type2); + abstract boolean accecptCh2(T type1, T type2); } } diff --git a/src/test/java/tests/ColocalisationTest.java b/src/test/java/tests/ColocalisationTest.java index e69e54c..7bcec0f 100644 --- a/src/test/java/tests/ColocalisationTest.java +++ b/src/test/java/tests/ColocalisationTest.java @@ -1,132 +1,126 @@ package tests; import gadgets.MaskFactory; import net.imglib2.RandomAccessibleInterval; import net.imglib2.algorithm.math.ImageStatistics; -import net.imglib2.img.Img; import net.imglib2.type.logic.BitType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.UnsignedByteType; import org.junit.After; import org.junit.Before; public abstract class ColocalisationTest { // images and meta data for zero correlation RandomAccessibleInterval zeroCorrelationImageCh1; RandomAccessibleInterval zeroCorrelationImageCh2; RandomAccessibleInterval zeroCorrelationAlwaysTrueMask; double zeroCorrelationImageCh1Mean; double zeroCorrelationImageCh2Mean; - // images and meta data for positive correlation test - // and real noisy image Manders' coeff with mask test + // images and meta data for positive correlation RandomAccessibleInterval positiveCorrelationImageCh1; RandomAccessibleInterval positiveCorrelationImageCh2; - // open mask image as a bit type cursor - Img positiveCorrelationMaskImage; RandomAccessibleInterval positiveCorrelationAlwaysTrueMask; double positiveCorrelationImageCh1Mean; double positiveCorrelationImageCh2Mean; // images and meta data for a synthetic negative correlation dataset RandomAccessibleInterval syntheticNegativeCorrelationImageCh1; RandomAccessibleInterval syntheticNegativeCorrelationImageCh2; RandomAccessibleInterval syntheticNegativeCorrelationAlwaysTrueMask; double syntheticNegativeCorrelationImageCh1Mean; double syntheticNegativeCorrelationImageCh2Mean; - // images like in the Manders paper + // images like in the manders paper RandomAccessibleInterval mandersA, mandersB, mandersC, mandersD, mandersE, mandersF, mandersG, mandersH, mandersI; RandomAccessibleInterval mandersAlwaysTrueMask; /** * This method is run before every single test is run and is meant to set up * the images and meta data needed for testing image colocalisation. */ @Before public void setup() { zeroCorrelationImageCh1 = TestImageAccessor.loadTiffFromJar("/greenZstack.tif"); zeroCorrelationImageCh1Mean = ImageStatistics.getImageMean(zeroCorrelationImageCh1); zeroCorrelationImageCh2 = TestImageAccessor.loadTiffFromJar("/redZstack.tif"); zeroCorrelationImageCh2Mean = ImageStatistics.getImageMean(zeroCorrelationImageCh2); final long[] dimZeroCorrCh1 = new long[ zeroCorrelationImageCh1.numDimensions() ]; zeroCorrelationImageCh1.dimensions(dimZeroCorrCh1); zeroCorrelationAlwaysTrueMask = MaskFactory.createMask(dimZeroCorrCh1, true); positiveCorrelationImageCh1 = TestImageAccessor.loadTiffFromJar("/colocsample1b-green.tif"); positiveCorrelationImageCh1Mean = ImageStatistics.getImageMean(positiveCorrelationImageCh1); positiveCorrelationImageCh2 = TestImageAccessor.loadTiffFromJar("/colocsample1b-red.tif"); positiveCorrelationImageCh2Mean = ImageStatistics.getImageMean(positiveCorrelationImageCh2); - - positiveCorrelationMaskImage = TestImageAccessor.loadTiffFromJarAsImg("/colocsample1b-mask.tif"); final long[] dimPosCorrCh1 = new long[ positiveCorrelationImageCh1.numDimensions() ]; positiveCorrelationImageCh1.dimensions(dimPosCorrCh1); positiveCorrelationAlwaysTrueMask = MaskFactory.createMask(dimPosCorrCh1, true); syntheticNegativeCorrelationImageCh1 = TestImageAccessor.loadTiffFromJar("/syntheticNegCh1.tif"); syntheticNegativeCorrelationImageCh1Mean = ImageStatistics.getImageMean(syntheticNegativeCorrelationImageCh1); syntheticNegativeCorrelationImageCh2 = TestImageAccessor.loadTiffFromJar("/syntheticNegCh2.tif"); syntheticNegativeCorrelationImageCh2Mean = ImageStatistics.getImageMean(syntheticNegativeCorrelationImageCh2); final long[] dimSynthNegCorrCh1 = new long[ syntheticNegativeCorrelationImageCh1.numDimensions() ]; syntheticNegativeCorrelationImageCh1.dimensions(dimSynthNegCorrCh1); syntheticNegativeCorrelationAlwaysTrueMask = MaskFactory.createMask(dimSynthNegCorrCh1, true); mandersA = TestImageAccessor.loadTiffFromJar("/mandersA.tiff"); mandersB = TestImageAccessor.loadTiffFromJar("/mandersB.tiff"); mandersC = TestImageAccessor.loadTiffFromJar("/mandersC.tiff"); mandersD = TestImageAccessor.loadTiffFromJar("/mandersD.tiff"); mandersE = TestImageAccessor.loadTiffFromJar("/mandersE.tiff"); mandersF = TestImageAccessor.loadTiffFromJar("/mandersF.tiff"); mandersG = TestImageAccessor.loadTiffFromJar("/mandersG.tiff"); mandersH = TestImageAccessor.loadTiffFromJar("/mandersH.tiff"); mandersI = TestImageAccessor.loadTiffFromJar("/mandersI.tiff"); final long[] dimMandersA = new long[ mandersA.numDimensions() ]; mandersA.dimensions(dimMandersA); mandersAlwaysTrueMask = MaskFactory.createMask(dimMandersA, true); } /** * This method is run after every single test and is meant to clean up. */ @After public void cleanup() { // nothing to do } /** * Creates a ROI offset array with a distance of 1/4 to the origin * in each dimension. */ protected > long[] createRoiOffset(RandomAccessibleInterval img) { final long[] offset = new long[ img.numDimensions() ]; img.dimensions(offset); for (int i=0; i> long[] createRoiSize(RandomAccessibleInterval img) { final long[] size = new long[ img.numDimensions() ]; img.dimensions(size); for (int i=0; i mc = new MandersColocalization(); TwinCursor cursor; MandersResults r; // test A-A combination cursor = new TwinCursor( mandersA.randomAccess(), mandersA.randomAccess(), Views.iterable(mandersAlwaysTrueMask).localizingCursor()); r = mc.calculateMandersCorrelation(cursor, mandersA.randomAccess().get().createVariable()); assertEquals(1.0d, r.m1, 0.0001); assertEquals(1.0d, r.m2, 0.0001); // test A-B combination cursor = new TwinCursor( mandersA.randomAccess(), mandersB.randomAccess(), Views.iterable(mandersAlwaysTrueMask).localizingCursor()); r = mc.calculateMandersCorrelation(cursor, mandersA.randomAccess().get()); assertEquals(0.75d, r.m1, 0.0001); assertEquals(0.75d, r.m2, 0.0001); // test A-C combination cursor = new TwinCursor( mandersA.randomAccess(), mandersC.randomAccess(), Views.iterable(mandersAlwaysTrueMask).localizingCursor()); r = mc.calculateMandersCorrelation(cursor, mandersA.randomAccess().get()); assertEquals(0.5d, r.m1, 0.0001); assertEquals(0.5d, r.m2, 0.0001); // test A-D combination cursor = new TwinCursor( mandersA.randomAccess(), mandersD.randomAccess(), Views.iterable(mandersAlwaysTrueMask).localizingCursor()); r = mc.calculateMandersCorrelation(cursor, mandersA.randomAccess().get()); assertEquals(0.25d, r.m1, 0.0001); assertEquals(0.25d, r.m2, 0.0001); // test A-E combination cursor = new TwinCursor( mandersA.randomAccess(), mandersE.randomAccess(), Views.iterable(mandersAlwaysTrueMask).localizingCursor()); r = mc.calculateMandersCorrelation(cursor, mandersA.randomAccess().get()); assertEquals(0.0d, r.m1, 0.0001); assertEquals(0.0d, r.m2, 0.0001); // test A-F combination cursor = new TwinCursor( mandersA.randomAccess(), mandersF.randomAccess(), Views.iterable(mandersAlwaysTrueMask).localizingCursor()); r = mc.calculateMandersCorrelation(cursor, mandersA.randomAccess().get()); assertEquals(0.25d, r.m1, 0.0001); assertEquals(0.3333d, r.m2, 0.0001); // test A-G combination.firstElement( cursor = new TwinCursor( mandersA.randomAccess(), mandersG.randomAccess(), Views.iterable(mandersAlwaysTrueMask).localizingCursor()); r = mc.calculateMandersCorrelation(cursor, mandersA.randomAccess().get()); assertEquals(0.25d, r.m1, 0.0001); assertEquals(0.50d, r.m2, 0.0001); // test A-H combination cursor = new TwinCursor( mandersA.randomAccess(), mandersH.randomAccess(), Views.iterable(mandersAlwaysTrueMask).localizingCursor()); r = mc.calculateMandersCorrelation(cursor, mandersA.randomAccess().get()); assertEquals(0.25d, r.m1, 0.0001); assertEquals(1.00d, r.m2, 0.0001); // test A-I combination cursor = new TwinCursor( mandersA.randomAccess(), mandersI.randomAccess(), Views.iterable(mandersAlwaysTrueMask).localizingCursor()); r = mc.calculateMandersCorrelation(cursor, mandersA.randomAccess().get()); assertEquals(0.083d, r.m1, 0.001); assertEquals(0.75d, r.m2, 0.0001); } - - /** - * This method tests real experimental noisy but - * biologically perfectly colocalized test images, - * using previously calculated autothresholds (.above mode) - * Amongst other things, hopefully it is sensitive to - * choosing the wrong channel to test for above threshold - */ - @Test - public void mandersRealNoisyImagesTest() throws MissingPreconditionException { - - MandersColocalization mrnc = - new MandersColocalization(); - - // test biologically perfect but noisy image coloc combination - // this cast is bad, so use Views.iterable instead. - //Cursor mask = Converters.convert((IterableInterval) positiveCorrelationMaskImage, - Cursor mask = Converters.convert(Views.iterable(positiveCorrelationMaskImage), - new Converter() { - - @Override - public void convert(UnsignedByteType arg0, BitType arg1) { - arg1.set(arg0.get() > 0); - } - }, new BitType()).cursor(); - - TwinCursor twinCursor; - MandersResults r; - // Manually set the thresholds for ch1 and ch2 with the results from a - // Costes Autothreshold using bisection implementation of regression, of the images used - UnsignedByteType thresholdCh1 = new UnsignedByteType(); - thresholdCh1.setInteger(70); - UnsignedByteType thresholdCh2 = new UnsignedByteType(); - thresholdCh2.setInteger(53); - //Set the threshold mode - ThresholdMode tMode; - tMode = ThresholdMode.Above; - // Set the TwinCursor to have the mask image channel, and 2 images. - twinCursor = new TwinCursor( - positiveCorrelationImageCh1.randomAccess(), - positiveCorrelationImageCh2.randomAccess(), - mask); - - // Use the constructor that takes ch1 and ch2 autothresholds and threshold mode. - r = mrnc.calculateMandersCorrelation(twinCursor, thresholdCh1, thresholdCh2, tMode); - - assertEquals(0.705665d, r.m1, 0.000001); - assertEquals(0.724752d, r.m2, 0.000001); - } } \ No newline at end of file diff --git a/src/test/java/tests/TestImageAccessor.java b/src/test/java/tests/TestImageAccessor.java index 043148c..2cbd5f3 100644 --- a/src/test/java/tests/TestImageAccessor.java +++ b/src/test/java/tests/TestImageAccessor.java @@ -1,419 +1,398 @@ package tests; import static org.junit.Assume.assumeNotNull; import algorithms.MissingPreconditionException; import gadgets.MaskFactory; import ij.ImagePlus; import ij.gui.NewImage; import ij.gui.Roi; import ij.io.Opener; import ij.process.ImageProcessor; import java.awt.Color; import java.io.BufferedInputStream; import java.io.InputStream; import java.util.Arrays; import net.imglib2.Cursor; import net.imglib2.Interval; import net.imglib2.Localizable; import net.imglib2.Point; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.TwinCursor; import net.imglib2.algorithm.gauss.Gauss; import net.imglib2.algorithm.math.ImageStatistics; -import net.imglib2.img.Img; import net.imglib2.img.ImagePlusAdapter; import net.imglib2.img.ImgFactory; import net.imglib2.img.array.ArrayImgFactory; import net.imglib2.type.NativeType; import net.imglib2.type.logic.BitType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.view.Views; /** * A class containing some testing helper methods. It allows * to open Tiffs from within the Jar file and can generate noise * images. * * @author Dan White & Tom Kazimiers */ public class TestImageAccessor { /* a static opener for opening images without the * need for creating every time a new opener */ static Opener opener = new Opener(); /** * Loads a Tiff file from within the jar. The given path is treated * as relative to this tests-package (i.e. "Data/test.tiff" refers * to the test.tiff in sub-folder Data). * * @param The wanted output type. * @param relPath The relative path to the Tiff file. * @return The file as ImgLib image. */ public static & NativeType> RandomAccessibleInterval loadTiffFromJar(String relPath) { InputStream is = TestImageAccessor.class.getResourceAsStream(relPath); BufferedInputStream bis = new BufferedInputStream(is); ImagePlus imp = opener.openTiff(bis, "The Test Image"); assumeNotNull(imp); return ImagePlusAdapter.wrap(imp); } - - /** - * Loads a Tiff file from within the jar to use as a mask Cursor. - * So we use Img which has a cursor() method. - * The given path is treated - * as relative to this tests-package (i.e. "Data/test.tiff" refers - * to the test.tiff in sub-folder Data). - * - * @param The wanted output type. - * @param relPath The relative path to the Tiff file. - * @return The file as ImgLib image. - */ - public static & NativeType> Img loadTiffFromJarAsImg(String relPath) { - InputStream is = TestImageAccessor.class.getResourceAsStream(relPath); - BufferedInputStream bis = new BufferedInputStream(is); - - ImagePlus imp = opener.openTiff(bis, "The Test Image"); - assumeNotNull(imp); - return ImagePlusAdapter.wrap(imp); - } /** * Creates a noisy image that is created by repeatedly adding points * with random intensity to the canvas. That way it tries to mimic the * way a microscope produces images. This convenience method uses the * default values of a point size of 3.0 and produces 5000 points. * After the creation the image is smoothed with a sigma of one in each * direction. * * @param The wanted output type. * @param width The image width. * @param height The image height. * @return The noise image. */ public static & NativeType> RandomAccessibleInterval produceNoiseImageSmoothed(T type, int width, int height) { return produceNoiseImageSmoothed(type, width, height, 3.0f, 5000, new double[] {1.0,1.0}); } /** * Creates a noisy image that is created by repeatedly adding points * with random intensity to the canvas. That way it tries to mimic the * way a microscope produces images. * * @param The wanted output type. * @param width The image width. * @param height The image height. * @param dotSize The size of the dots. * @param numDots The number of dots. * @param smoothingSigma The two dimensional sigma for smoothing. * @return The noise image. */ public static & NativeType> RandomAccessibleInterval produceNoiseImage(int width, int height, float dotSize, int numDots) { /* For now (probably until ImageJ2 is out) we use an * ImageJ image to draw circles. */ int options = NewImage.FILL_BLACK + NewImage.CHECK_AVAILABLE_MEMORY; ImagePlus img = NewImage.createByteImage("Noise", width, height, 1, options); ImageProcessor imp = img.getProcessor(); float dotRadius = dotSize * 0.5f; int dotIntSize = (int) dotSize; for (int i=0; i < numDots; i++) { int x = (int) (Math.random() * width - dotRadius); int y = (int) (Math.random() * height - dotRadius); imp.setColor(Color.WHITE); imp.fillOval(x, y, dotIntSize, dotIntSize); } // we changed the data, so update it img.updateImage(); // create the new image RandomAccessibleInterval noiseImage = ImagePlusAdapter.wrap(img); return noiseImage; } public static & NativeType> RandomAccessibleInterval produceNoiseImageSmoothed(T type, int width, int height, float dotSize, int numDots, double[] smoothingSigma) { RandomAccessibleInterval noiseImage = produceNoiseImage(width, height, dotSize, numDots); return gaussianSmooth(noiseImage, smoothingSigma); } /** * This method creates a noise image that has a specified mean. * Every pixel has a value uniformly distributed around mean with * the maximum spread specified. * * @return a new noise image * @throws MissingPreconditionException if specified means and spreads are not valid */ public static & NativeType> RandomAccessibleInterval produceMeanBasedNoiseImage(T type, int width, int height, double mean, double spread, double[] smoothingSigma) throws MissingPreconditionException { if (mean < spread || (mean + spread) > type.getMaxValue()) { throw new MissingPreconditionException("Mean must be larger than spread, and mean plus spread must be smaller than max of the type"); } // create the new image ImgFactory imgFactory = new ArrayImgFactory(); RandomAccessibleInterval noiseImage = imgFactory.create( new int[] {width, height}, type); // "Noise image"); for (T value : Views.iterable(noiseImage)) { value.setReal( mean + ( (Math.random() - 0.5) * spread ) ); } return gaussianSmooth(noiseImage, smoothingSigma); } /** * This method creates a noise image that is made of many little * sticks oriented in a random direction. How many of them and * what the length of them are can be specified. * * @return a new noise image that is not smoothed */ public static & NativeType> RandomAccessibleInterval produceSticksNoiseImage(int width, int height, int numSticks, int lineWidth, double maxLength) { /* For now (probably until ImageJ2 is out) we use an * ImageJ image to draw lines. */ int options = NewImage.FILL_BLACK + NewImage.CHECK_AVAILABLE_MEMORY; ImagePlus img = NewImage.createByteImage("Noise", width, height, 1, options); ImageProcessor imp = img.getProcessor(); imp.setColor(Color.WHITE); imp.setLineWidth(lineWidth); for (int i=0; i < numSticks; i++) { // find random starting point int x = (int) (Math.random() * width); int y = (int) (Math.random() * height); // create random stick length and direction double length = Math.random() * maxLength; double angle = Math.random() * 2 * Math.PI; // calculate random point on circle, for the direction int destX = x + (int) (length * Math.cos(angle)); int destY = y + (int) (length * Math.sin(angle)); // now draw the line imp.drawLine(x, y, destX, destY); } // we changed the data, so update it img.updateImage(); return ImagePlusAdapter.wrap(img); } /** * This method creates a smoothed noise image that is made of * many little sticks oriented in a random direction. How many * of them and what the length of them are can be specified. * * @return a new noise image that is smoothed */ public static & NativeType> RandomAccessibleInterval produceSticksNoiseImageSmoothed(T type, int width, int height, int numSticks, int lineWidth, double maxLength, double[] smoothingSigma) { RandomAccessibleInterval noiseImage = produceSticksNoiseImage(width, height, numSticks, lineWidth, maxLength); return gaussianSmooth(noiseImage, smoothingSigma); } /** * Generates a Perlin noise image. It is based on Ken Perlin's * reference implementation (ImprovedNoise class) and a small * bit of Kas Thomas' sample code (http://asserttrue.blogspot.com/). */ public static & NativeType> RandomAccessibleInterval producePerlinNoiseImage(T type, int width, int height, double z, double scale) { // create the new image ImgFactory imgFactory = new ArrayImgFactory(); RandomAccessibleInterval noiseImage = imgFactory.create( new int[] {width, height}, type); Cursor noiseCursor = Views.iterable(noiseImage).localizingCursor(); double xOffset = Math.random() * (width*width); double yOffset = Math.random() * (height*height); while (noiseCursor.hasNext()) { noiseCursor.fwd(); double x = (noiseCursor.getDoublePosition(0) + xOffset) * scale; double y = (noiseCursor.getDoublePosition(1) + yOffset) * scale; float t = (float)ImprovedNoise.noise( x, y, z); // ImprovedNoise.noise returns a float in the range [-1..1], // whereas we want a float in the range [0..1], so: t = (1 + t) * 0.5f; noiseCursor.get().setReal(t); } //return gaussianSmooth(noiseImage, imgFactory, smoothingSigma); return noiseImage; } /** * Gaussian Smooth of the input image using intermediate float format. * @param * @param img * @param sigma * @return */ public static & NativeType> RandomAccessibleInterval gaussianSmooth( RandomAccessibleInterval img, double[] sigma) { Interval interval = Views.iterable(img); ImgFactory outputFactory = new ArrayImgFactory(); final long[] dim = new long[ img.numDimensions() ]; img.dimensions(dim); RandomAccessibleInterval output = outputFactory.create( dim, img.randomAccess().get().createVariable() ); final long[] pos = new long[ img.numDimensions() ]; Arrays.fill(pos, 0); Localizable origin = new Point(pos); ImgFactory tempFactory = new ArrayImgFactory(); RandomAccessible input = Views.extendMirrorSingle(img); Gauss.inFloat(sigma, input, interval, output, origin, tempFactory); return output; } /** * Inverts an image. * * @param The images data type. * @param image The image to convert. * @return The inverted image. */ public static & NativeType> RandomAccessibleInterval invertImage( RandomAccessibleInterval image) { Cursor imgCursor = Views.iterable(image).localizingCursor(); // invert the image long[] dim = new long[ image.numDimensions() ]; image.dimensions(dim); ArrayImgFactory imgFactory = new ArrayImgFactory(); RandomAccessibleInterval invImg = imgFactory.create( dim, image.randomAccess().get().createVariable() ); // "Inverted " + image.getName()); RandomAccess invCursor = invImg.randomAccess(); while (imgCursor.hasNext()) { imgCursor.fwd(); invCursor.setPosition(imgCursor); invCursor.get().setReal( imgCursor.get().getMaxValue() - imgCursor.get().getRealDouble() ); } return invImg; } /** * Converts an arbitrary image to a black/white version of it. * All image data lower or equal 0.5 times the maximum value * of the image type will get black, the rest will turn white. */ public static & NativeType> RandomAccessibleInterval makeBinaryImage( RandomAccessibleInterval image) { T binSplitValue = image.randomAccess().get(); binSplitValue.setReal( binSplitValue.getMaxValue() * 0.5 ); return TestImageAccessor.makeBinaryImage(image, binSplitValue); } /** * Converts an arbitrary image to a black/white version of it. * All image data lower or equal the splitValue will get black, * the rest will turn white. */ public static & NativeType> RandomAccessibleInterval makeBinaryImage( RandomAccessibleInterval image, T splitValue) { Cursor imgCursor = Views.iterable(image).localizingCursor(); // make a new image of the same type, but binary long[] dim = new long[ image.numDimensions() ]; image.dimensions(dim); ArrayImgFactory imgFactory = new ArrayImgFactory(); RandomAccessibleInterval binImg = imgFactory.create( dim, image.randomAccess().get().createVariable() ); // "Binary image of " + image.getName()); RandomAccess invCursor = binImg.randomAccess(); while (imgCursor.hasNext()) { imgCursor.fwd(); invCursor.setPosition(imgCursor); T currentValue = invCursor.get(); if (currentValue.compareTo(splitValue) > 0) currentValue.setReal( currentValue.getMaxValue() ); else currentValue.setZero(); } return binImg; } /** * A method to combine a foreground image and a background image. * If data on the foreground image is above zero, it will be * placed on the background. While doing that, the image data from * the foreground is scaled to be in range of the background. */ public static > void combineImages(RandomAccessibleInterval background, RandomAccessibleInterval foreground) { final long[] dim = new long[ background.numDimensions() ]; background.dimensions(dim); RandomAccessibleInterval alwaysTrueMask = MaskFactory.createMask(dim, true); TwinCursor cursor = new TwinCursor( background.randomAccess(), foreground.randomAccess(), Views.iterable(alwaysTrueMask).localizingCursor()); // find a scaling factor for scale forground range into background double bgMin = ImageStatistics.getImageMin(background).getRealDouble(); double bgMax = ImageStatistics.getImageMax(background).getRealDouble(); double fgMin = ImageStatistics.getImageMin(foreground).getRealDouble(); double fgMax = ImageStatistics.getImageMax(foreground).getRealDouble(); double scaling = (bgMax - bgMin ) / (fgMax - fgMin); // iterate over both images while (cursor.hasNext()) { cursor.fwd(); T bgData = cursor.getFirst(); double fgData = cursor.getSecond().getRealDouble() * scaling; if (fgData > 0.01) { /* if the foreground data is above zero, copy * it to the background. */ bgData.setReal(fgData); } } } /** * Creates a mask image with a black background and a white * rectangular foreground. * * @param width The width of the result image. * @param height The height of the result image. * @param offset The offset of the rectangular mask. * @param size The size of the rectangular mask. * @return A black image with a white rectangle on it. */ public static & NativeType> RandomAccessibleInterval createRectengularMaskImage( long width, long height, long[] offset, long[] size) { /* For now (probably until ImageJ2 is out) we use an * ImageJ image to draw lines. */ int options = NewImage.FILL_BLACK + NewImage.CHECK_AVAILABLE_MEMORY; ImagePlus img = NewImage.createByteImage("Noise", (int)width, (int)height, 1, options); ImageProcessor imp = img.getProcessor(); imp.setColor(Color.WHITE); Roi rect = new Roi(offset[0], offset[1], size[0], size[1]); imp.fill(rect); // we changed the data, so update it img.updateImage(); return ImagePlusAdapter.wrap(img); } } diff --git a/src/test/resources/colocsample1b-mask.tif b/src/test/resources/colocsample1b-mask.tif deleted file mode 100644 index d03ab6a..0000000 Binary files a/src/test/resources/colocsample1b-mask.tif and /dev/null differ