From 764e680d7cd1ab07ca21bc7b27935afff75ce110 Mon Sep 17 00:00:00 2001 From: felmer <felmer> Date: Mon, 22 Sep 2014 13:47:26 +0000 Subject: [PATCH] SSDM-773: Handle 12-bit microscopy images correctly in order to allow useful user defined transformation. SVN: 32471 --- .../cisd/common/image/ImageHistogram.java | 3 +- .../cisd/common/image/IntensityRescaling.java | 179 +++++++----------- .../common/image/IntensityRescalingTest.java | 26 ++- .../dss/generic/shared/utils/ImageUtil.java | 2 +- ...scaleIntensityImageTransformerFactory.java | 34 +--- .../BitShiftingImageTransformerFactory.java | 7 +- ...IntensityRangeImageTransformerFactory.java | 13 +- ...scaleIntensityImageTransformerFactory.java | 31 +-- .../BitShiftingImageTransformerFactory.java | 5 +- ...IntensityRangeImageTransformerFactory.java | 7 +- .../v1/SimpleImageDataSetRegistrator.java | 3 +- .../v2/SimpleImageDataSetRegistrator.java | 3 +- .../server/images/ImageChannelsUtils.java | 33 +++- .../openbis/dss/shared/DssScreeningUtils.java | 45 ++++- 14 files changed, 209 insertions(+), 182 deletions(-) diff --git a/common/source/java/ch/systemsx/cisd/common/image/ImageHistogram.java b/common/source/java/ch/systemsx/cisd/common/image/ImageHistogram.java index 6d3be675bf5..295cc480ca3 100644 --- a/common/source/java/ch/systemsx/cisd/common/image/ImageHistogram.java +++ b/common/source/java/ch/systemsx/cisd/common/image/ImageHistogram.java @@ -299,8 +299,7 @@ public class ImageHistogram } } - - private boolean isGray() + public boolean isGray() { for (int i = 0; i < redCounters.length; i++) { diff --git a/common/source/java/ch/systemsx/cisd/common/image/IntensityRescaling.java b/common/source/java/ch/systemsx/cisd/common/image/IntensityRescaling.java index d819acb75da..209e2ab5a90 100644 --- a/common/source/java/ch/systemsx/cisd/common/image/IntensityRescaling.java +++ b/common/source/java/ch/systemsx/cisd/common/image/IntensityRescaling.java @@ -123,60 +123,6 @@ public class IntensityRescaling } } - private static int getGrayIntensity(BufferedImage image, int x, int y) - { - return image.getRaster().getSample(x, y, 0); - } - - private static int getBitShiftLowerThanThreshold(int[] b0, float pixels, float threshold) - { - int shift = b0.length - 1; - while (shift >= 0 && (b0[shift] / pixels) < threshold) - { - --shift; - } - return shift + 1; - } - - /** - * Computes the number of significant bits in an image, minus 8. A bit position is considered - * significant if only a small fraction (given by <var>threshold</var> of all pixels has a value - * of 1 in this bit position. - * <p> - * For example, if the image is 16-bit and only uses 10-bits, this method will return 2. - * - * @param image The image to compute the bits for. - * @param threshold The threshold of pixels (divided by the total number of pixels) that can be - * '1' in bit position so that the bit-position is still consider insignificant. A - * typical value will be 0.001f (one per-mill). - * @return The number of significant bits of the intensity minus 8. - */ - public static int computeBitShift(BufferedImage image, float threshold) - { - if (isNotGrayscale(image)) - { - throw new IllegalArgumentException( - "computeBitShift() is only applicable to gray scale images."); - } - float pixels = image.getWidth() * image.getHeight(); - final int[] b0 = new int[image.getColorModel().getPixelSize() - 8]; - for (int y = 0; y < image.getHeight(); ++y) - { - for (int x = 0; x < image.getWidth(); ++x) - { - final int intensity = getGrayIntensity(image, x, y); - for (int b = 0; b < b0.length; ++b) - { - if (((intensity >>> (b + 8)) & 1) == 1) - { - ++b0[b]; - } - } - } - } - return getBitShiftLowerThanThreshold(b0, pixels, threshold); - } - /** @return true if the specified image in not in grayscale */ public static boolean isNotGrayscale(BufferedImage image) { @@ -216,22 +162,6 @@ public class IntensityRescaling return channels; } - /** - * Performs an intensity rescaling on a gray-scale image by shifting all intensities so that - * only significant bits are kept. A bit position is considered significant if only a small - * fraction (given by <var>threshold</var> of all pixels has a value of 1 in this bit position. - * - * @param image The original n-bit gray-scale image (n>8). - * @param threshold The threshold of pixels (divided by the total number of pixels) that can be - * '1' in bit position so that the bit-position is still consider insignificant. A - * typical value will be 0.001f (one per-mill). - * @return The rescaled 8-bit gray-scale image. - */ - public static BufferedImage rescaleIntensityBitShiftTo8Bits(BufferedImage image, float threshold) - { - return rescaleIntensityBitShiftTo8Bits(image, computeBitShift(image, threshold)); - } - /** * Performs an intensity rescaling on a gray-scale image by shifting all intensities by * <var>shiftBits</var> bits. @@ -240,13 +170,7 @@ public class IntensityRescaling * @param shiftBits The number of bits to shift the image by. * @return The rescaled 8-bit gray-scale image. */ - public static BufferedImage rescaleIntensityBitShiftTo8Bits(BufferedImage image, int shiftBits) - { - return rescaleIntensityBitShiftTo8Bits(new Pixels(image), shiftBits); - } - - /** See {@link #rescaleIntensityBitShiftTo8Bits(BufferedImage, int)}. */ - private static BufferedImage rescaleIntensityBitShiftTo8Bits(Pixels image, int shiftBits) + public static BufferedImage rescaleIntensityBitShiftTo8Bits(Pixels image, int shiftBits) { final BufferedImage rescaledImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); @@ -268,25 +192,15 @@ public class IntensityRescaling /** * Process <var>image</var> and add its pixels to the <var>histogram</var>. Calling this method * multiple times with the same <var>histogram</var> accumulates the histogram for all images. - * - * @param image a gray scale image (will not be checked) */ - public static void addToLevelStats(PixelHistogram histogram, BufferedImage image) - { - Pixels pixels = new Pixels(image); - addToLevelStats(histogram, pixels); - } - - /** - * Process <var>image</var> and add its pixels to the <var>histogram</var>. Calling this method - * multiple times with the same <var>histogram</var> accumulates the histogram for all images. - */ - private static void addToLevelStats(PixelHistogram histogram, Pixels pixels) + public static void addToLevelStats(PixelHistogram histogram, Pixels pixels, Channel... channels) { + assert channels.length > 0 : "No channels specified."; int[] histogramArray = histogram.getHistogram(); int[][] pixelData = pixels.getPixelData(); - for (int[] channelPixelData : pixelData) + for (Channel channel : channels) { + int[] channelPixelData = pixelData[channel.getBand()]; for (int i = 0; i < channelPixelData.length; i++) { histogramArray[channelPixelData[i]]++; @@ -313,18 +227,35 @@ public class IntensityRescaling width = image.getWidth(); height = image.getHeight(); ColorModel colorModel = image.getColorModel(); - pixelData = new int[colorModel.getNumColorComponents()][width * height]; + int numColorComponents = colorModel.getNumColorComponents(); + pixelData = new int[numColorComponents][width * height]; WritableRaster raster = image.getRaster(); int numberOfBands = raster.getNumBands(); - if (colorModel instanceof IndexColorModel == false && numberOfBands >= pixelData.length) + int[][] colorIndexMap = null; + if (numColorComponents == 3 && numberOfBands == 1) + { + colorIndexMap = tryCreateColorIndexMap(colorModel); + } + if (numberOfBands >= pixelData.length || colorIndexMap != null) { - for (int band = 0; band < pixelData.length; band++) + for (int band = 0, n = Math.min(numberOfBands, pixelData.length); band < n; band++) { raster.getSamples(0, 0, width, height, band, pixelData[band]); } + if (colorIndexMap != null) + { + for (int i = 0; i < pixelData[0].length; i++) + { + int index = pixelData[0][i]; + for (int c = 0; c < 3; c++) + { + pixelData[c][i] = colorIndexMap[c][index]; + } + } + } } else { - // In case of an IndexColorModel or + // In case of the color model isn't a recognized index color model and // number of bands is less then number of color components // we can not use the fast method for (int y = 0; y < height; y++) @@ -342,6 +273,46 @@ public class IntensityRescaling } } + /** + * Creates the index color map from the specified color model. The result is an array + * of three integer arrays. The first/second/third array is the red/green/blue index to color map. + * <p> + * This method only handles {@link IndexColorModel} for 8bit indicies. To handle other + * index color models (like loci.formats.gui.Index16ColorModel from the BioFormats library) this + * method should be overwritten. + * + * @return <code>null</code> if the color model isn't a known index color model. + * + */ + protected int[][] tryCreateColorIndexMap(ColorModel colorModel) + { + if (colorModel instanceof IndexColorModel == false) + { + return null; + } + IndexColorModel indexColorModel = (IndexColorModel) colorModel; + int mapSize = indexColorModel.getMapSize(); + byte[] blues = new byte[mapSize]; + indexColorModel.getBlues(blues); + byte[] greens = new byte[mapSize]; + indexColorModel.getGreens(greens); + byte[] reds = new byte[mapSize]; + indexColorModel.getReds(reds); + int[][] result = new int[3][mapSize]; + copyTo(reds, result[0]); + copyTo(greens, result[1]); + copyTo(blues, result[2]); + return result; + } + + private void copyTo(byte[] bytes, int[] integers) + { + for (int i = 0; i < bytes.length; i++) + { + integers[i] = bytes[i] & 0xff; + } + } + /** @return all the pixels of the image */ public int[][] getPixelData() { @@ -410,18 +381,17 @@ public class IntensityRescaling } /** - * Calculates levels for the specified image. It starts with a threshold 0.01 (i.e. cut-off of points + * Calculates levels for the specified pixels. It starts with a threshold 0.01 (i.e. cut-off of points * to be too light or dark). If the number of levels is less than the specified minimum the threshold * is reduced by a factor 10 and the levels are calculated again. This iteration is continue until * either the number of levels is large enough or the threshold is below 10<sup>-5</sup>. */ - public static Levels computeLevels(BufferedImage image, int minimumNumberOfLevels) + public static Levels computeLevels(Pixels pixels, int minimumNumberOfLevels) { - Pixels pixels = new Pixels(image); Levels levels = null; for (float threshold = 0.01f; threshold > 1e-5; threshold /= 10) { - levels = computeLevels(pixels, threshold); + levels = computeLevels(pixels, threshold, Channel.values()); if (levels.getMaxLevel() - levels.getMinLevel() > minimumNumberOfLevels) { break; @@ -439,10 +409,11 @@ public class IntensityRescaling * @param image a gray scale image (will not be checked). * @return The levels of the <var>histogram</var> for the <var>treshold</var>. */ - public static Levels computeLevels(Pixels image, float threshold) + public static Levels computeLevels(Pixels image, float threshold, Channel...channels) { + assert channels.length > 0 : "No channels specified."; final PixelHistogram stats = new PixelHistogram(); - addToLevelStats(stats, image); + addToLevelStats(stats, image, channels); return computeLevels(stats, threshold); } @@ -450,14 +421,10 @@ public class IntensityRescaling * Computes an intensity rescaled image from the given <var>image</var>, using the black point * and white point as defined by <var>levels</var>. */ - public static BufferedImage rescaleIntensityLevelTo8Bits(BufferedImage image, Levels levels) - { - return rescaleIntensityLevelTo8Bits(new Pixels(image), levels, Channel.values()); - } - public static BufferedImage rescaleIntensityLevelTo8Bits(Pixels pixels, Levels levels, Channel... channels) { + assert channels.length > 0 : "No channels specified."; final int width = pixels.getWidth(); final int height = pixels.getHeight(); int[][] pixelData = pixels.getPixelData(); diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/image/IntensityRescalingTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/image/IntensityRescalingTest.java index 2170cc31265..c835c10d590 100644 --- a/common/sourceTest/java/ch/systemsx/cisd/common/image/IntensityRescalingTest.java +++ b/common/sourceTest/java/ch/systemsx/cisd/common/image/IntensityRescalingTest.java @@ -21,6 +21,7 @@ import static org.testng.AssertJUnit.assertEquals; import java.awt.Color; import java.awt.Graphics; import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; import org.testng.annotations.Test; @@ -103,7 +104,8 @@ public class IntensityRescalingTest graphics.setColor(Color.PINK); graphics.fillRect(1, 1, 4, 3); - BufferedImage rescaledImage = IntensityRescaling.rescaleIntensityLevelTo8Bits(image, new Levels(75, 190)); + BufferedImage rescaledImage = IntensityRescaling.rescaleIntensityLevelTo8Bits(new Pixels(image), + new Levels(75, 190), Channel.values()); ImageHistogram histogram = ImageHistogram.calculateHistogram(rescaledImage); @@ -112,6 +114,28 @@ public class IntensityRescalingTest assertEquals("[0=18, 222=12]", renderHistogram(histogram.getBlueHistogram())); } + @Test + public void testRescaleIntensityLevelForAnIndexColorModel() + { + BufferedImage image = new BufferedImage(6, 5, BufferedImage.TYPE_BYTE_INDEXED); + ColorModel colorModel = image.getColorModel(); + assertEquals("IndexColorModel", colorModel.getClass().getSimpleName()); + Graphics graphics = image.getGraphics(); + graphics.setColor(Color.ORANGE); + graphics.fillRect(0, 0, 4, 3); + graphics.setColor(Color.PINK); + graphics.fillRect(1, 1, 4, 3); + + BufferedImage rescaledImage = IntensityRescaling.rescaleIntensityLevelTo8Bits(new Pixels(image), + new Levels(75, 190), Channel.values()); + + ImageHistogram histogram = ImageHistogram.calculateHistogram(rescaledImage); + + assertEquals("[0=12, 255=18]", renderHistogram(histogram.getRedHistogram())); + assertEquals("[0=12, 173=12, 255=6]", renderHistogram(histogram.getGreenHistogram())); + assertEquals("[0=18, 173=12]", renderHistogram(histogram.getBlueHistogram())); + } + @Test public void testRescaleIntensityLevelTo8BitsForGrayExample() { diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ImageUtil.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ImageUtil.java index 2fe24534b3f..18a674cac83 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ImageUtil.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ImageUtil.java @@ -925,7 +925,7 @@ public class ImageUtil { Pixels pixels = new Pixels(image); Levels intensityRange = IntensityRescaling.computeLevels(pixels, - threshold == null ? DEFAULT_IMAGE_OPTIMAL_RESCALING_FACTOR : threshold); + threshold == null ? DEFAULT_IMAGE_OPTIMAL_RESCALING_FACTOR : threshold, Channel.RED); BufferedImage result = IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, intensityRange, Channel.RED); return result; diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/AutoRescaleIntensityImageTransformerFactory.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/AutoRescaleIntensityImageTransformerFactory.java index 771daec22aa..d81dcd2bdb1 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/AutoRescaleIntensityImageTransformerFactory.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/AutoRescaleIntensityImageTransformerFactory.java @@ -17,7 +17,6 @@ package ch.systemsx.cisd.openbis.dss.etl.dto.api.transformations; import java.awt.image.BufferedImage; -import java.awt.image.WritableRaster; import ch.systemsx.cisd.base.annotation.JsonObject; import ch.systemsx.cisd.base.image.IImageTransformer; @@ -27,11 +26,12 @@ import ch.systemsx.cisd.common.image.IntensityRescaling.Channel; import ch.systemsx.cisd.common.image.IntensityRescaling.Levels; import ch.systemsx.cisd.common.image.IntensityRescaling.Pixels; import ch.systemsx.cisd.openbis.dss.generic.shared.utils.ImageUtil; +import ch.systemsx.cisd.openbis.dss.shared.DssScreeningUtils; /** * Transformation performed by - * {@link IntensityRescaling#rescaleIntensityLevelTo8Bits(BufferedImage, Levels)} where levels are - * computed automatically by {@link IntensityRescaling#computeLevels(Pixels, float)}. + * {@link IntensityRescaling#rescaleIntensityLevelTo8Bits(Pixels, Levels, Channel...)} where levels are + * computed automatically by {@link IntensityRescaling#computeLevels(Pixels, float, Channel...)}. * <p> * Warning: The serialized version of this class can be stored in the database for each image. * Moving this class to a different package or changing it in a backward incompatible way would make @@ -66,32 +66,14 @@ public class AutoRescaleIntensityImageTransformerFactory implements IImageTransf { return image; } - Pixels grayScaleImage = toGrayScale(image, channel); - Levels levels = IntensityRescaling.computeLevels(grayScaleImage, threshold); - return IntensityRescaling.rescaleIntensityLevelTo8Bits(image, levels); + Pixels pixels = DssScreeningUtils.createPixels(image); + Levels levels = IntensityRescaling.computeLevels(pixels, threshold, channel); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, Channel.values()); } - Pixels pixels = new Pixels(image); - Levels levels = IntensityRescaling.computeLevels(pixels, threshold); + Pixels pixels = DssScreeningUtils.createPixels(image); + Levels levels = IntensityRescaling.computeLevels(pixels, threshold, Channel.RED); return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, Channel.RED); } }; } - - private Pixels toGrayScale(BufferedImage image, Channel channel) - { - BufferedImage gray = - new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - WritableRaster raster = gray.getRaster(); - - for (int y = 0; y < image.getHeight(); y++) - { - for (int x = 0; x < image.getWidth(); x++) - { - int value = (image.getRGB(x, y) >> channel.getShift()) & 0xff; - raster.setPixel(x, y, new int[] - { value }); - } - } - return new Pixels(gray); - } } diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/BitShiftingImageTransformerFactory.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/BitShiftingImageTransformerFactory.java index 5917b60a0cc..e828fc8f24c 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/BitShiftingImageTransformerFactory.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/BitShiftingImageTransformerFactory.java @@ -6,10 +6,12 @@ import ch.systemsx.cisd.base.annotation.JsonObject; import ch.systemsx.cisd.base.image.IImageTransformer; import ch.systemsx.cisd.base.image.IImageTransformerFactory; import ch.systemsx.cisd.common.image.IntensityRescaling; +import ch.systemsx.cisd.common.image.IntensityRescaling.Pixels; +import ch.systemsx.cisd.openbis.dss.shared.DssScreeningUtils; /** * Transformation performed by - * {@link IntensityRescaling#rescaleIntensityBitShiftTo8Bits(java.awt.image.BufferedImage, int)} + * {@link IntensityRescaling#rescaleIntensityBitShiftTo8Bits(Pixels, int)} * <p> * Warning: The serialized version of this class can be stored in the database for each image. * Moving this class to a different package or changing it in a backward incompatible way would make @@ -41,7 +43,8 @@ final class BitShiftingImageTransformerFactory implements IImageTransformerFacto { return image; } - return IntensityRescaling.rescaleIntensityBitShiftTo8Bits(image, shiftBits); + Pixels pixels = DssScreeningUtils.createPixels(image); + return IntensityRescaling.rescaleIntensityBitShiftTo8Bits(pixels, shiftBits); } }; } diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/IntensityRangeImageTransformerFactory.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/IntensityRangeImageTransformerFactory.java index 502d5ccdc05..832dcc05b92 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/IntensityRangeImageTransformerFactory.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/transformations/IntensityRangeImageTransformerFactory.java @@ -26,9 +26,10 @@ import ch.systemsx.cisd.common.image.IntensityRescaling; import ch.systemsx.cisd.common.image.IntensityRescaling.Channel; import ch.systemsx.cisd.common.image.IntensityRescaling.Levels; import ch.systemsx.cisd.common.image.IntensityRescaling.Pixels; +import ch.systemsx.cisd.openbis.dss.shared.DssScreeningUtils; /** - * Transformation performed by {@link IntensityRescaling#rescaleIntensityLevelTo8Bits(BufferedImage, Levels)}. + * Transformation performed by {@link IntensityRescaling#rescaleIntensityLevelTo8Bits(Pixels, Levels, Channel...)}. * <p> * Warning: The serialized version of this class can be stored in the database for each image. Moving this class to a different package or changing it * in a backward incompatible way would make all the saved transformations invalid. @@ -71,15 +72,17 @@ public class IntensityRangeImageTransformerFactory implements IImageTransformerF Levels levels = new Levels(blackPointIntensity, whitePointIntensity); if (IntensityRescaling.isNotGrayscale(image)) { + Pixels pixels = DssScreeningUtils.createPixels(image); EnumSet<Channel> channels = IntensityRescaling.getUsedRgbChannels(image); if (channels.size() != 1) { - return IntensityRescaling.rescaleIntensityLevelTo8Bits(image, levels); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, Channel.values()); } - return IntensityRescaling.rescaleIntensityLevelTo8Bits(new Pixels(image), levels, - channels.iterator().next()); + Channel channel = channels.iterator().next(); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, channel); } - return IntensityRescaling.rescaleIntensityLevelTo8Bits(image, levels); + Pixels pixels = DssScreeningUtils.createPixels(image); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, Channel.values()); } }; } diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/AutoRescaleIntensityImageTransformerFactory.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/AutoRescaleIntensityImageTransformerFactory.java index 83b2000e3cc..61fa01749a7 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/AutoRescaleIntensityImageTransformerFactory.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/AutoRescaleIntensityImageTransformerFactory.java @@ -17,7 +17,6 @@ package ch.systemsx.cisd.openbis.dss.etl.dto.api.v1.transformations; import java.awt.image.BufferedImage; -import java.awt.image.WritableRaster; import ch.systemsx.cisd.base.annotation.JsonObject; import ch.systemsx.cisd.base.image.IImageTransformer; @@ -27,6 +26,7 @@ import ch.systemsx.cisd.common.image.IntensityRescaling.Channel; import ch.systemsx.cisd.common.image.IntensityRescaling.Levels; import ch.systemsx.cisd.common.image.IntensityRescaling.Pixels; import ch.systemsx.cisd.openbis.dss.generic.shared.utils.ImageUtil; +import ch.systemsx.cisd.openbis.dss.shared.DssScreeningUtils; /** * This class is obsolete, and should not be used. Use @@ -62,33 +62,14 @@ public class AutoRescaleIntensityImageTransformerFactory implements IImageTransf { return image; } - Pixels grayScaleImage = toGrayScale(image, channel); - Levels levels = IntensityRescaling.computeLevels(grayScaleImage, threshold); - return IntensityRescaling.rescaleIntensityLevelTo8Bits(image, levels); + Pixels pixels = DssScreeningUtils.createPixels(image); + Levels levels = IntensityRescaling.computeLevels(pixels, threshold, channel); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, Channel.values()); } - Pixels pixels = new Pixels(image); - Levels levels = IntensityRescaling.computeLevels(pixels, threshold); + Pixels pixels = DssScreeningUtils.createPixels(image); + Levels levels = IntensityRescaling.computeLevels(pixels, threshold, Channel.RED); return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, Channel.RED); } }; } - - private Pixels toGrayScale(BufferedImage image, Channel channel) - { - BufferedImage gray = - new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - WritableRaster raster = gray.getRaster(); - - for (int y = 0; y < image.getHeight(); y++) - { - for (int x = 0; x < image.getWidth(); x++) - { - int value = (image.getRGB(x, y) >> channel.getShift()) & 0xff; - raster.setPixel(x, y, new int[] - { value }); - } - } - return new Pixels(gray); - } - } diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/BitShiftingImageTransformerFactory.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/BitShiftingImageTransformerFactory.java index 0ac8971dade..637943f2ee6 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/BitShiftingImageTransformerFactory.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/BitShiftingImageTransformerFactory.java @@ -6,6 +6,8 @@ import ch.systemsx.cisd.base.annotation.JsonObject; import ch.systemsx.cisd.base.image.IImageTransformer; import ch.systemsx.cisd.base.image.IImageTransformerFactory; import ch.systemsx.cisd.common.image.IntensityRescaling; +import ch.systemsx.cisd.common.image.IntensityRescaling.Pixels; +import ch.systemsx.cisd.openbis.dss.shared.DssScreeningUtils; /** * This class is obsolete, and should not be used. Use {@link ch.systemsx.cisd.openbis.dss.etl.dto.api.transformations.ConvertToolImageTransformer} @@ -38,7 +40,8 @@ final class BitShiftingImageTransformerFactory implements IImageTransformerFacto { return image; } - return IntensityRescaling.rescaleIntensityBitShiftTo8Bits(image, shiftBits); + Pixels pixels = DssScreeningUtils.createPixels(image); + return IntensityRescaling.rescaleIntensityBitShiftTo8Bits(pixels, shiftBits); } }; } diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/IntensityRangeImageTransformerFactory.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/IntensityRangeImageTransformerFactory.java index 56d775f1927..677849d5319 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/IntensityRangeImageTransformerFactory.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/IntensityRangeImageTransformerFactory.java @@ -26,6 +26,7 @@ import ch.systemsx.cisd.common.image.IntensityRescaling; import ch.systemsx.cisd.common.image.IntensityRescaling.Channel; import ch.systemsx.cisd.common.image.IntensityRescaling.Levels; import ch.systemsx.cisd.common.image.IntensityRescaling.Pixels; +import ch.systemsx.cisd.openbis.dss.shared.DssScreeningUtils; /** * This class is obsolete, and should not be used. Use @@ -76,12 +77,14 @@ public class IntensityRangeImageTransformerFactory implements IImageTransformerF } else { Levels levels = new Levels(blackPointIntensity, whitePointIntensity); - return IntensityRescaling.rescaleIntensityLevelTo8Bits(new Pixels(image), levels, + Pixels pixels = DssScreeningUtils.createPixels(image); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, channels.iterator().next()); } } Levels levels = new Levels(blackPointIntensity, whitePointIntensity); - return IntensityRescaling.rescaleIntensityLevelTo8Bits(image, levels); + Pixels pixels = DssScreeningUtils.createPixels(image); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, Channel.values()); } }; } diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/v1/SimpleImageDataSetRegistrator.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/v1/SimpleImageDataSetRegistrator.java index 79406de96f3..79e8b93e8d5 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/v1/SimpleImageDataSetRegistrator.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/v1/SimpleImageDataSetRegistrator.java @@ -602,7 +602,8 @@ public class SimpleImageDataSetRegistrator imageFile.getPath())); return null; } - IntensityRescaling.addToLevelStats(histogram, image); + IntensityRescaling.addToLevelStats(histogram, DssScreeningUtils.createPixels(image), + ch.systemsx.cisd.common.image.IntensityRescaling.Channel.RED); } } return IntensityRescaling.computeLevels(histogram, threshold); diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/v2/SimpleImageDataSetRegistrator.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/v2/SimpleImageDataSetRegistrator.java index c3cada40072..b6737bb912a 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/v2/SimpleImageDataSetRegistrator.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/v2/SimpleImageDataSetRegistrator.java @@ -610,7 +610,8 @@ public class SimpleImageDataSetRegistrator imageFile.getPath())); return null; } - IntensityRescaling.addToLevelStats(histogram, image); + IntensityRescaling.addToLevelStats(histogram, DssScreeningUtils.createPixels(image), + ch.systemsx.cisd.common.image.IntensityRescaling.Channel.RED); } } catch (Exception ex) { diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtils.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtils.java index f7a853d70d9..73c5bdeb110 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtils.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtils.java @@ -61,6 +61,7 @@ import ch.systemsx.cisd.openbis.dss.generic.shared.IHierarchicalContentProvider; import ch.systemsx.cisd.openbis.dss.generic.shared.ServiceProvider; import ch.systemsx.cisd.openbis.dss.generic.shared.dto.Size; import ch.systemsx.cisd.openbis.dss.generic.shared.utils.ImageUtil; +import ch.systemsx.cisd.openbis.dss.shared.DssScreeningUtils; import ch.systemsx.cisd.openbis.generic.shared.dto.OpenBISSessionHolder; import ch.systemsx.cisd.openbis.plugin.screening.shared.api.v1.dto.ImageRepresentationFormat; import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ScreeningConstants; @@ -396,6 +397,7 @@ public class ImageChannelsUtils BufferedImage image = calculateSingleImage(imageReference); image = transform(image, imageReference, transformationInfo); image = ImageUtil.convertForDisplayIfNecessary(image, threshold); + image = convertTo8Bit(image); Channel channel = ImageUtil.getRepresentativeChannelIfEffectiveGray(image); if (channel != null) { @@ -448,7 +450,30 @@ public class ImageChannelsUtils } return image; } - + + private static BufferedImage convertTo8Bit(BufferedImage image) + { + Pixels pixels = DssScreeningUtils.createPixels(image); + int[][] pixelData = pixels.getPixelData(); + int maxIntensity = 0; + for (int[] colorChannelPixels : pixelData) + { + for (int intensity : colorChannelPixels) + { + if (intensity > maxIntensity) + { + maxIntensity = intensity; + } + } + } + if (maxIntensity < 256) + { + return image; + } + Levels levels = new Levels(0, maxIntensity < 4096 ? 4096 : 1 << 16); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, Channel.values()); + } + private static IColorTransformation createColorTransformation(final Channel channel, final ChannelColorRGB channelColor) { return new IColorTransformation() @@ -515,7 +540,7 @@ public class ImageChannelsUtils IImageTransformerFactory channelTransformation = mergedChannelTransformationOrNull; if ((transMap == null || transMap.size() == 0) && channelTransformation == null) { - Levels levels = IntensityRescaling.computeLevels(mergedImage, 30); + Levels levels = IntensityRescaling.computeLevels(DssScreeningUtils.createPixels(mergedImage), 30); int minLevel = levels.getMinLevel(); int maxLevel = levels.getMaxLevel(); channelTransformation = new IntensityRangeImageTransformerFactory(minLevel, maxLevel); @@ -593,7 +618,6 @@ public class ImageChannelsUtils ImageUtil.DEFAULT_IMAGE_OPTIMAL_RESCALING_FACTOR); } } - return applyImageTransformation(resultImage, channelLevelTransformationOrNull); } @@ -763,7 +787,7 @@ public class ImageChannelsUtils public static BufferedImage transformColor(BufferedImage bufferedImage, IColorTransformation transformation) { - Pixels pixels = new Pixels(bufferedImage); + Pixels pixels = DssScreeningUtils.createPixels(bufferedImage); int width = pixels.getWidth(); int height = pixels.getHeight(); int[][] pixelData = pixels.getPixelData(); @@ -791,5 +815,4 @@ public class ImageChannelsUtils final byte[] output = ImageUtil.imageToPngFast(image); return new ByteArrayBasedContentNode(output, nameOrNull); } - } diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/shared/DssScreeningUtils.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/shared/DssScreeningUtils.java index 97f2e0987e4..0b901e63473 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/shared/DssScreeningUtils.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/shared/DssScreeningUtils.java @@ -16,11 +16,16 @@ package ch.systemsx.cisd.openbis.dss.shared; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; + import javax.sql.DataSource; +import loci.formats.gui.Index16ColorModel; import net.lemnik.eodsql.QueryTool; import ch.systemsx.cisd.base.image.IImageTransformerFactory; +import ch.systemsx.cisd.common.image.IntensityRescaling.Pixels; import ch.systemsx.cisd.openbis.dss.generic.shared.ServiceProvider; import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ScreeningConstants; import ch.systemsx.cisd.openbis.plugin.screening.shared.imaging.dataaccess.IImagingReadonlyQueryDAO; @@ -50,8 +55,7 @@ public class DssScreeningUtils } /** - * Creates a DAO based on imaging database specified in DSS service.properties by data source - * {@link ScreeningConstants#IMAGING_DATA_SOURCE}. + * Creates a DAO based on imaging database specified in DSS service.properties by data source {@link ScreeningConstants#IMAGING_DATA_SOURCE}. * <p> * Returned query is reused and should not be closed. * </p> @@ -65,8 +69,7 @@ public class DssScreeningUtils } /** - * Creates a new query each time when it is called. Returned query should be closed after all - * operations are done. + * Creates a new query each time when it is called. Returned query should be closed after all operations are done. */ public static IImagingTransformerDAO createImagingTransformerDAO() { @@ -76,4 +79,38 @@ public class DssScreeningUtils return QueryTool.getQuery(dataSource, IImagingTransformerDAO.class); } + /** + * Creates {@link Pixels} wrapper object for the specified image which can also handle + * 16bit index color models. Such color models are used when reading Nikon microscopy + * image files with ND2Reader of BioFormats. + */ + public static Pixels createPixels(BufferedImage image) + { + return new Pixels(image) + { + @Override + protected int[][] tryCreateColorIndexMap(ColorModel colorModel) + { + int[][] indexMap = super.tryCreateColorIndexMap(colorModel); + if (indexMap == null && colorModel instanceof Index16ColorModel) + { + Index16ColorModel indexColorModel = (Index16ColorModel) colorModel; + indexMap = new int[3][1 << 16]; + copyTo(indexColorModel.getReds(), indexMap[0]); + copyTo(indexColorModel.getGreens(), indexMap[1]); + copyTo(indexColorModel.getBlues(), indexMap[2]); + } + return indexMap; + } + + private void copyTo(short[] shorts, int[] integers) + { + for (int i = 0; i < shorts.length; i++) + { + integers[i] = shorts[i] & 0xffff; + } + } + }; + } + } -- GitLab