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