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 209e2ab5a9090b5597b904bf3a237e2a95834b13..d15f37f59f868d624dcb1573fdc0622c3ab2770c 100644 --- a/common/source/java/ch/systemsx/cisd/common/image/IntensityRescaling.java +++ b/common/source/java/ch/systemsx/cisd/common/image/IntensityRescaling.java @@ -200,12 +200,16 @@ public class IntensityRescaling int[][] pixelData = pixels.getPixelData(); for (Channel channel : channels) { - int[] channelPixelData = pixelData[channel.getBand()]; - for (int i = 0; i < channelPixelData.length; i++) + int band = channel.getBand(); + if (band < pixelData.length) { - histogramArray[channelPixelData[i]]++; + int[] channelPixelData = pixelData[band]; + for (int i = 0; i < channelPixelData.length; i++) + { + histogramArray[channelPixelData[i]]++; + } + histogram.pixelCount += channelPixelData.length; } - histogram.pixelCount += channelPixelData.length; } } @@ -332,6 +336,11 @@ public class IntensityRescaling } } + + public static interface IImageToPixelsConverter + { + public Pixels convert(BufferedImage image); + } /** * Computes the levels (black point and white point) of the given <var>histogram</var> for the diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/test/ImageDebugViewer.java b/common/sourceTest/java/ch/systemsx/cisd/common/test/ImageDebugViewer.java index 63c6cde1ca5d42ede50eb4b461a9c3993157d220..8f2e6cf7e2b1e339af3a147af4dc00f3e173b0eb 100644 --- a/common/sourceTest/java/ch/systemsx/cisd/common/test/ImageDebugViewer.java +++ b/common/sourceTest/java/ch/systemsx/cisd/common/test/ImageDebugViewer.java @@ -157,7 +157,7 @@ public class ImageDebugViewer System.out.println("\\_________ " + title); } - private static String tryGetTypeAsString(BufferedImage image) + public static String tryGetTypeAsString(BufferedImage image) { int type = image.getType(); return type >= 0 && type < IMAGE_TYPES.length ? IMAGE_TYPES[type] : null; diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/AbstractDatasetDownloadServlet.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/AbstractDatasetDownloadServlet.java index ea06230a93d4c07ca3edf8655405d1da5999937d..7ae7cb8df0aff1571b7e144dc10e6cbd4d3c5302 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/AbstractDatasetDownloadServlet.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/AbstractDatasetDownloadServlet.java @@ -39,6 +39,7 @@ import org.apache.log4j.Logger; import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.common.image.IntensityRescaling.IImageToPixelsConverter; import ch.systemsx.cisd.common.logging.LogCategory; import ch.systemsx.cisd.common.logging.LogFactory; import ch.systemsx.cisd.openbis.common.io.hierarchical_content.api.IHierarchicalContentNode; @@ -254,17 +255,18 @@ abstract public class AbstractDatasetDownloadServlet extends HttpServlet } protected static final BufferedImage createThumbnail(IHierarchicalContentNode fileNode, - Size thumbnailSize) + Size thumbnailSize, IImageToPixelsConverter converterOrNull) { - BufferedImage image = ImageUtil.loadImageForDisplay(fileNode); - return createThumbnail(image, thumbnailSize); + BufferedImage image = ImageUtil.loadImageForDisplay(fileNode, converterOrNull); + return createThumbnail(image, thumbnailSize, converterOrNull); } - protected static final BufferedImage createThumbnail(BufferedImage image, Size thumbnailSize) + protected static final BufferedImage createThumbnail(BufferedImage image, Size thumbnailSize, + IImageToPixelsConverter converterOrNull) { int width = thumbnailSize.getWidth(); int height = thumbnailSize.getHeight(); - return ImageUtil.createThumbnailForDisplay(image, width, height); + return ImageUtil.createThumbnailForDisplay(image, width, height, converterOrNull); } // if display mode describes a thumbnail return its expected size diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadServlet.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadServlet.java index 1067e55249207085299201e213d8d9d7009482b4..b9e7166af414431118bf5db8ac8d70ff3969e7c8 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadServlet.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadServlet.java @@ -495,7 +495,7 @@ public class DatasetDownloadServlet extends AbstractDatasetDownloadServlet Size thumbnailSize = tryAsThumbnailDisplayMode(displayMode); if (thumbnailSize != null) { - BufferedImage image = createThumbnail(fileNode, thumbnailSize); + BufferedImage image = createThumbnail(fileNode, thumbnailSize, null); infoPostfix = " as a thumbnail."; responseStream = createResponseContentStream(image, fileNode.getName()); } else 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 7b323ec843837b1399267b5bfc4ac0bb4f51af16..cdcdae98a699e3e69751c6bf59ac3b3a9f6c5d81 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 @@ -20,6 +20,7 @@ import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -52,6 +53,7 @@ import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; import ch.systemsx.cisd.common.image.ImageHistogram; import ch.systemsx.cisd.common.image.IntensityRescaling; import ch.systemsx.cisd.common.image.IntensityRescaling.Channel; +import ch.systemsx.cisd.common.image.IntensityRescaling.IImageToPixelsConverter; import ch.systemsx.cisd.common.image.IntensityRescaling.Levels; import ch.systemsx.cisd.common.image.IntensityRescaling.Pixels; import ch.systemsx.cisd.common.logging.LogCategory; @@ -881,7 +883,8 @@ public class ImageUtil * * @throws IllegalArgumentException if the file isn't a valid image file. */ - public static BufferedImage loadImageForDisplay(IHierarchicalContentNode contentNode) + public static BufferedImage loadImageForDisplay(IHierarchicalContentNode contentNode, + IImageToPixelsConverter converterOrNull) { if (contentNode.exists() == false) { @@ -889,7 +892,7 @@ public class ImageUtil + contentNode.getRelativePath()); } BufferedImage result = loadImage(contentNode); - result = convertForDisplayIfNecessary(result, null); + result = convertForDisplayIfNecessary(result, null, converterOrNull); return result; } @@ -906,37 +909,91 @@ public class ImageUtil * @param maxHeight Maximum height of the result image. */ public static BufferedImage createThumbnailForDisplay(BufferedImage image, int maxWidth, - int maxHeight) + int maxHeight, IImageToPixelsConverter converterOrNull) { - BufferedImage result = rescale(image, maxWidth, maxHeight, true, false); - result = convertForDisplayIfNecessary(result, null); + BufferedImage result = rescale(image, maxWidth, maxHeight, true, false, converterOrNull); + result = convertForDisplayIfNecessary(result, null, converterOrNull); return result; } /** * If the specified image uses grayscale with color depth larger then 8 bits, conversion to 8 * bits grayscale is done. Otherwise the original image is returned. + * <p> + * Conversion is done by intensity rescaling (with outlier cutoff specified by <code>threshold</code>) + * because the actual resolution is often not know because getMaxNumberOfBitsPerComponent() might + * return 16 even tough the resolution is only 12. */ - public static BufferedImage convertForDisplayIfNecessary(BufferedImage image, Float threshold) + private static BufferedImage convertForDisplayIfNecessary(BufferedImage image, Float threshold, + IImageToPixelsConverter converterOrNull) + { + Channel channel = getRepresentativeChannelIfEffectiveGrayAndMoreThan8Bit(image); + if (channel != null) + { + Pixels pixels = converterOrNull == null ? new Pixels(image) : converterOrNull.convert(image); + Levels intensityRange = IntensityRescaling.computeLevels(pixels, + threshold == null ? DEFAULT_IMAGE_OPTIMAL_RESCALING_FACTOR : threshold, channel); + convertToGray(pixels); + BufferedImage result = + IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, intensityRange, Channel.values()); + return result; + } + return image; + } + + private static void convertToGray(Pixels pixels) { - if (isGrayscale(image)) + int[][] pixelData = pixels.getPixelData(); + if (pixelData.length > 1) { - if (image.getColorModel().getPixelSize() > 8) + for (int i = 0, n = pixelData[0].length; i < n; i++) { - Pixels pixels = new Pixels(image); - Levels intensityRange = IntensityRescaling.computeLevels(pixels, - threshold == null ? DEFAULT_IMAGE_OPTIMAL_RESCALING_FACTOR : threshold, Channel.RED); - BufferedImage result = - IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, intensityRange, Channel.RED); - return result; + int max = 0; + for (int c = 0; c < pixelData.length; c++) + { + max = Math.max(max, pixelData[c][i]); + } + for (int c = 0; c < pixelData.length; c++) + { + pixelData[c][i] = max; + } } } - return image; } - private static boolean isGrayscale(BufferedImage image) + private static Channel getRepresentativeChannelIfEffectiveGrayAndMoreThan8Bit(BufferedImage image) + { + if (getMaxNumberOfBitsPerComponent(image) <= 8) + { + return null; + } + ColorModel colorModel = image.getColorModel(); + if (image.getType() != BufferedImage.TYPE_CUSTOM) + { + return colorModel.getNumColorComponents() == 1 ? Channel.RED : null; + } + return getRepresentativeChannelIfEffectiveGray(image); + } + + /** + * Returns the maximum bit resolution of the specified image. It returns the maximum of the array + * returned by {@link ColorModel#getComponentSize()}. If not defined (which should be only the case + * for Index16ColorModel of the BioFormats library) {@link ColorModel#getPixelSize()} is returned. + */ + public static int getMaxNumberOfBitsPerComponent(BufferedImage image) { - return image.getColorModel().getColorSpace().getNumComponents() == 1; + ColorModel colorModel = image.getColorModel(); + int[] componentSize = colorModel.getComponentSize(); + if (componentSize == null) + { + return colorModel.getPixelSize(); + } + int max = 0; + for (int size : componentSize) + { + max = Math.max(max, size); + } + return max; } /** @@ -953,13 +1010,12 @@ public class ImageUtil checkIfChannelIsUsed(histogramsByChannels, numberOfPixels, Channel.RED, imageHistogram.getRedHistogram()); checkIfChannelIsUsed(histogramsByChannels, numberOfPixels, Channel.GREEN, imageHistogram.getGreenHistogram()); checkIfChannelIsUsed(histogramsByChannels, numberOfPixels, Channel.BLUE, imageHistogram.getBlueHistogram()); - List<Entry<Channel, int[]>> usedChannels = new ArrayList<Map.Entry<Channel,int[]>>(histogramsByChannels.entrySet()); - if (usedChannels.isEmpty()) + if (histogramsByChannels.isEmpty()) { return Channel.RED; // Black image is a gray image, doesn't matter which channel to return. } - Entry<Channel, int[]> representativeChannel = usedChannels.get(0); - int[] representativeHistogram = representativeChannel.getValue(); + List<Entry<Channel, int[]>> usedChannels = new ArrayList<Map.Entry<Channel,int[]>>(histogramsByChannels.entrySet()); + int[] representativeHistogram = usedChannels.get(0).getValue(); for (int i = 1; i < usedChannels.size(); i++) { int[] histogram = usedChannels.get(i).getValue(); @@ -971,7 +1027,7 @@ public class ImageUtil } } } - return representativeChannel.getKey(); + return usedChannels.get(0).getKey(); } private static void checkIfChannelIsUsed(Map<Channel, int[]> usedChannels, int numberOfPixels, Channel channel, int[] histogram) @@ -993,9 +1049,10 @@ public class ImageUtil * specified limit, then the image is not changed. * @param highQuality8Bit if true thumbnails will be of higher quality, but rescaling will take * longer and the image will be converted to 8 bit. + * @param converterOrNull */ public static BufferedImage rescale(BufferedImage image, int maxWidth, int maxHeight, - boolean enlargeIfNecessary, boolean highQuality8Bit) + boolean enlargeIfNecessary, boolean highQuality8Bit, IImageToPixelsConverter converterOrNull) { int width = image.getWidth(); int height = image.getHeight(); @@ -1028,7 +1085,7 @@ public class ImageUtil // WORKAROUND: non-default interpolations do not work well with 16 bit grayscale images. // We have to rescale colors to 8 bit here, otherwise the result will contain only few // colors. - imageToRescale = convertForDisplayIfNecessary(imageToRescale, 0f); + imageToRescale = convertForDisplayIfNecessary(imageToRescale, 0f, converterOrNull); } graphics2D.drawImage(imageToRescale, 0, 0, thumbnailWidth, thumbnailHeight, null); graphics2D.dispose(); @@ -1064,7 +1121,7 @@ public class ImageUtil } else if (imageType == BufferedImage.TYPE_BYTE_INDEXED) { imageType = BufferedImage.TYPE_INT_RGB; - } else + } else { imageType = isTransparent ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB; } diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ImageUtilTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ImageUtilTest.java index 16f8790de14065579de07ca0a400bf3417f67da2..d1aeb542ed1f92b714b0a9bf6c9f0c04daffbab3 100644 --- a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ImageUtilTest.java +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ImageUtilTest.java @@ -195,8 +195,8 @@ public class ImageUtilTest extends AssertJUnit public void testCreateThumbnail() { BufferedImage image = loadImageByFile("gif-example.gif"); - assertImageSize(79, 100, ImageUtil.createThumbnailForDisplay(image, 200, 100)); - assertImageSize(100, 127, ImageUtil.createThumbnailForDisplay(image, 100, 200)); + assertImageSize(79, 100, ImageUtil.createThumbnailForDisplay(image, 200, 100, null)); + assertImageSize(100, 127, ImageUtil.createThumbnailForDisplay(image, 100, 200, null)); } @Test diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ThumbnailTiming.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ThumbnailTiming.java index c9398b194fb451bf03f9f99ea67f7b77bf52946e..1206c96e505a3361bd7f239d394b4c96a486972e 100644 --- a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ThumbnailTiming.java +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/ThumbnailTiming.java @@ -45,7 +45,7 @@ public class ThumbnailTiming BufferedImage image = ImageUtil.loadImage(file); stopWatch.stop(); stopWatch.start("Convert"); - BufferedImage thumbnail = ImageUtil.createThumbnailForDisplay(image, 120, 60); + BufferedImage thumbnail = ImageUtil.createThumbnailForDisplay(image, 120, 60, null); stopWatch.stop(); stopWatch.start("Write"); try diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_256x256.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_256x256.png index 64f766e96cdd8e30306eaacf593c2d2aee6be593..505aa720a4c75a971361b0382aa9c6b82904e515 100644 Binary files a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_256x256.png and b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_256x256.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_512x512.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_512x512.png index 3f9d4dd60c8c97b5b7fa13b356d2fbed2c32b3a1..bff740a7587447527129340eab3a081882229b22 100644 Binary files a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_512x512.png and b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_512x512.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_Default.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_Default.png index 8eb9f9719b1c18624d51d26d21abeb258051b8e8..9a8f6d162486844641f59db6497bab9621894eb0 100644 Binary files a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_Default.png and b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_Default.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_256x256.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_256x256.png index 606a7eec9f055c41eb7ef8b53db4d921c0ec5b62..a70ad683741b97b3202ee4cb0b2db6bb716daf20 100644 Binary files a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_256x256.png and b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_256x256.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_512x512.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_512x512.png index 7b4c17c9926914721535db24618655cbac24026c..ed1e2ca44d046b24808689711220b182d53fd8c6 100644 Binary files a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_512x512.png and b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_512x512.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_Default.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_Default.png index 23e66f27906dc14733b7503b70c3375b533ee66b..e4700d5497dcda8e103f7b9767a4ed471a8b3fef 100644 Binary files a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_Default.png and b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_Default.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C2_256x256.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C2_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..e86ec46c89142e7630d87a367a9111b5c47e5f85 Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C2_256x256.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C2_512x512.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C2_512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..9a5ae3414f0383c8c086bc18c1f11e6e8d5ff68f Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C2_512x512.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C2_Default.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C2_Default.png new file mode 100644 index 0000000000000000000000000000000000000000..f3f7abcbcea27ad7714fb0c3aef6a43f79b77297 Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C2_Default.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256.png index 7d5970d587d62f5e42e0b6fddc47b860b822cbfa..276c76c906fcd1637581412340c1fbc9b26be380 100644 Binary files a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256.png and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256_C0_0_200_C4_150_300.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256_C0_0_200_C4_150_300.png new file mode 100644 index 0000000000000000000000000000000000000000..a819fc3fae5bd3dfcfba6501044c35fa635171a2 Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256_C0_0_200_C4_150_300.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256_C0_0_20_C4_2_15.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256_C0_0_20_C4_2_15.png deleted file mode 100644 index 81c735e05c9337d3566b0cacd4410009a4df5fc2..0000000000000000000000000000000000000000 Binary files a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256_C0_0_20_C4_2_15.png and /dev/null differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_512x512.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_512x512.png index d9f5808b9cfb84581c8023afbd56315ed9826a5a..d4016582c089abed3b9f21c8b795418efedb99a2 100644 Binary files a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_512x512.png and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_512x512.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_Default.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_Default.png index 93a958c35c792678c82ef7af9736173921a60d34..b973b64c693f4da9700e1c47f323230e5f04b685 100644 Binary files a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_Default.png and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_Default.png differ diff --git a/screening/resource/test-data/Simple16BitImageDropboxTest/1_2_Merged_1392x1040.png b/screening/resource/test-data/Simple16BitImageDropboxTest/1_2_Merged_1392x1040.png index 3b2405c6cb58587185e20ebf26d3a2aac18a9aa2..966de5b24e497ecf05385ff34d34942d94542ac4 100644 Binary files a/screening/resource/test-data/Simple16BitImageDropboxTest/1_2_Merged_1392x1040.png and b/screening/resource/test-data/Simple16BitImageDropboxTest/1_2_Merged_1392x1040.png differ diff --git a/screening/resource/test-data/Simple16BitImageDropboxTest/PLATE-16-BIT/PLATE-16-BIT_A01_06_DAPI.png b/screening/resource/test-data/Simple16BitImageDropboxTest/PLATE-16-BIT/PLATE-16-BIT_A01_06_DAPI.png new file mode 100644 index 0000000000000000000000000000000000000000..fadfd88558039643f8e5b807607490de47292181 Binary files /dev/null and b/screening/resource/test-data/Simple16BitImageDropboxTest/PLATE-16-BIT/PLATE-16-BIT_A01_06_DAPI.png differ diff --git a/screening/resource/test-data/Simple16BitImageDropboxTest/PLATE-16-BIT/PLATE-16-BIT_A01_06_GFP.png b/screening/resource/test-data/Simple16BitImageDropboxTest/PLATE-16-BIT/PLATE-16-BIT_A01_06_GFP.png new file mode 100644 index 0000000000000000000000000000000000000000..48035210735d6717cd3ed57ad9e9690ab6776e71 Binary files /dev/null and b/screening/resource/test-data/Simple16BitImageDropboxTest/PLATE-16-BIT/PLATE-16-BIT_A01_06_GFP.png differ diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbsoluteImageReference.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbsoluteImageReference.java index 39d4e54d68d72f7ae77e902bfecff5ec6016d623..8d108164ed632db04c806acbb846e57811eadd43 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbsoluteImageReference.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbsoluteImageReference.java @@ -143,8 +143,7 @@ public class AbsoluteImageReference extends AbstractImageReference { return image.getColorModel().getPixelSize(); } - return Utils.loadUnchangedImageColorDepth(contentNode, channelCodeOrNull, - imageLibraryOrNull); + return Utils.loadUnchangedImageColorDepth(contentNode, tryGetImageID(), imageLibraryOrNull); } public RequestedImageSize getRequestedSize() diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/Hdf5ThumbnailGenerator.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/Hdf5ThumbnailGenerator.java index b0e4a87b87101bf25a1da56e0868e6820382cb84..9e15f914f4c7b9137897a520c99d1b7a8ec00be7 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/Hdf5ThumbnailGenerator.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/Hdf5ThumbnailGenerator.java @@ -65,6 +65,7 @@ import ch.systemsx.cisd.openbis.dss.etl.dto.api.transformations.ImageTransformat import ch.systemsx.cisd.openbis.dss.generic.server.images.ImageChannelsUtils; 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.plugin.screening.shared.imaging.dataaccess.ColorComponent; /** @@ -444,7 +445,7 @@ public class Hdf5ThumbnailGenerator implements IHDF5WriterClient } boolean highQuality = thumbnailsStorageFormat.isHighQuality(); - BufferedImage rescaledImage = ImageUtil.rescale(image, widht, height, false, highQuality); + BufferedImage rescaledImage = ImageUtil.rescale(image, widht, height, false, highQuality, DssScreeningUtils.CONVERTER); final List<ThumbnailData> thumbnails = new ArrayList<ThumbnailData>(); for (String channelCode : getChannelsToProcess(imageFileInfo.getChannelCode())) @@ -475,26 +476,14 @@ public class Hdf5ThumbnailGenerator implements IHDF5WriterClient private BufferedImage applyTransformationsChain(BufferedImage image, ColorComponent colorComponent, IImageTransformer transformer) { - return applyTransformationIfNeeded(extractSingleChannelIfNeeded(image, colorComponent), - transformer); + BufferedImage extractedChannelImage = ImageChannelsUtils.extractChannel(image, colorComponent); + return applyTransformationIfNeeded(extractedChannelImage, transformer); } private BufferedImage applyTransformationsChain(BufferedImage image, String channelCode, ColorComponent colorComponent) { - return applyTransformationsChain(image, colorComponent, - tryCreateImageTransformer(channelCode)); - } - - private static BufferedImage extractSingleChannelIfNeeded(BufferedImage image, - ColorComponent colorComponent) - { - if (colorComponent != null) - { - return ImageChannelsUtils.transformToChannel(image, colorComponent); - } - - return image; + return applyTransformationsChain(image, colorComponent, tryCreateImageTransformer(channelCode)); } private static BufferedImage applyTransformationIfNeeded(BufferedImage image, 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 73c5bdeb110911aca96c950bd099997dd7f3da5e..dc8c8d5784515aa01342fb274a42c7aef4c7c61c 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 @@ -31,9 +31,11 @@ import ch.rinn.restrictions.Private; import ch.systemsx.cisd.base.image.IImageTransformer; import ch.systemsx.cisd.base.image.IImageTransformerFactory; import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.common.image.ImageHistogram; 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.PixelHistogram; import ch.systemsx.cisd.common.image.IntensityRescaling.Pixels; import ch.systemsx.cisd.common.image.MixColors; import ch.systemsx.cisd.common.logging.LogCategory; @@ -394,86 +396,54 @@ public class ImageChannelsUtils AbsoluteImageReference imageReference, ImageTransformationParams transformationInfo, Float threshold) { - 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) - { - final ChannelColorRGB channelColor = imageReference.getChannelColor(); - image = transformColor(image, createColorTransformation(channel, channelColor)); - } + BufferedImage image = imageReference.getUnchangedImage(); + image = rescaleIfNot8Bit(image, threshold); + image = resize(image, imageReference.getRequestedSize()); + image = extractChannel(image, imageReference.tryGetColorComponent()); + image = transform(image, imageReference, transformationInfo, threshold); + image = transformGrayToColor(image, imageReference.getChannelColor()); return image; } - - private static BufferedImage calculateSingleImage(AbsoluteImageReference imageReference) - { - long start = operationLog.isDebugEnabled() ? System.currentTimeMillis() : 0; - BufferedImage image = imageReference.getUnchangedImage(); - - if (operationLog.isDebugEnabled()) - { - operationLog.debug("Load original image: " + (System.currentTimeMillis() - start)); - } - - // resized the image if necessary - RequestedImageSize requestedSize = imageReference.getRequestedSize(); - Size size = requestedSize.tryGetThumbnailSize(); - if (size != null) + private static BufferedImage rescaleIfNot8Bit(BufferedImage image, Float threshold) + { + if (ImageUtil.getMaxNumberOfBitsPerComponent(image) <= 8) { - start = operationLog.isDebugEnabled() ? System.currentTimeMillis() : 0; - boolean enlarge = requestedSize.enlargeIfNecessary(); - boolean highQuality8Bit = requestedSize.isHighQualityRescalingRequired(); - image = ImageUtil.rescale(image, size.getWidth(), size.getHeight(), enlarge, highQuality8Bit); - if (highQuality8Bit == false) - { - image = ImageUtil.convertForDisplayIfNecessary(image, null); - } - if (operationLog.isDebugEnabled()) - { - operationLog.debug("Create thumbnail: " + (System.currentTimeMillis() - start)); - } + return image; } + Pixels pixels = DssScreeningUtils.createPixels(image); + float thresholdValue = getThresholdValue(threshold); + Levels levels = IntensityRescaling.computeLevels(pixels, thresholdValue, Channel.values()); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, Channel.values()); + } - // choose color component if necessary - final ColorComponent colorComponentOrNull = imageReference.tryGetColorComponent(); - if (colorComponentOrNull != null) - { - start = operationLog.isDebugEnabled() ? System.currentTimeMillis() : 0; - image = transformToChannel(image, colorComponentOrNull); - if (operationLog.isDebugEnabled()) - { - operationLog.debug("Select single channel: " + (System.currentTimeMillis() - start)); - } - } - return image; + private static float getThresholdValue(Float threshold) + { + return threshold == null ? ImageUtil.DEFAULT_IMAGE_OPTIMAL_RESCALING_FACTOR : threshold; } - private static BufferedImage convertTo8Bit(BufferedImage image) + private static BufferedImage transformGrayToColor(BufferedImage image, final ChannelColorRGB channelColor) { - Pixels pixels = DssScreeningUtils.createPixels(image); - int[][] pixelData = pixels.getPixelData(); - int maxIntensity = 0; - for (int[] colorChannelPixels : pixelData) + Channel channel = ImageUtil.getRepresentativeChannelIfEffectiveGray(image); + if (channel == null) { - for (int intensity : colorChannelPixels) - { - if (intensity > maxIntensity) - { - maxIntensity = intensity; - } - } + return image; } - if (maxIntensity < 256) + return transformColor(image, createColorTransformation(channel, channelColor)); + } + + private static BufferedImage resize(BufferedImage image, RequestedImageSize requestedSize) + { + Size size = requestedSize.tryGetThumbnailSize(); + if (size == null) { return image; } - Levels levels = new Levels(0, maxIntensity < 4096 ? 4096 : 1 << 16); - return IntensityRescaling.rescaleIntensityLevelTo8Bits(pixels, levels, Channel.values()); + boolean enlarge = requestedSize.enlargeIfNecessary(); + boolean highQuality8Bit = requestedSize.isHighQualityRescalingRequired(); + return ImageUtil.rescale(image, size.getWidth(), size.getHeight(), enlarge, highQuality8Bit, DssScreeningUtils.CONVERTER); } - + private static IColorTransformation createColorTransformation(final Channel channel, final ChannelColorRGB channelColor) { return new IColorTransformation() @@ -516,29 +486,19 @@ public class ImageChannelsUtils ImageTransformationParams transformationInfo, IImageTransformerFactory mergedChannelTransformationOrNull) { - // We do not transform single images here. - IImageCalculator calculator = createCalculator(transformationInfo); - List<ImageWithReference> images = calculateSingleImages(imageReferences, calculator); - for (int i = 0; i < images.size(); i++) + assert transformationInfo != null; + List<ImageWithReference> images = new ArrayList<ImageWithReference>(); + for (AbsoluteImageReference imageReference : imageReferences) { - ImageWithReference imageWithReference = images.get(i); - BufferedImage image = imageWithReference.getBufferedImage(); - AbsoluteImageReference imageReference = imageWithReference.getReference(); - Channel channel = ImageUtil.getRepresentativeChannelIfEffectiveGray(image); - if (channel != null) - { - final ChannelColorRGB channelColor = imageReference.getChannelColor(); - image = transformColor(image, createColorTransformation(channel, channelColor)); - imageWithReference.setImage(image); - } + images.add(new ImageWithReference(imageReference.getUnchangedImage(), imageReference)); } - + calculateImagesForMerging(images, transformationInfo); BufferedImage mergedImage = mergeImages(images); // non-user transformation - apply color range fix after mixing Map<String, String> transMap = transformationInfo.tryGetTransformationCodeForChannels(); IImageTransformerFactory channelTransformation = mergedChannelTransformationOrNull; - if ((transMap == null || transMap.size() == 0) && channelTransformation == null) + if ((transMap == null || transMap.isEmpty()) && channelTransformation == null) { Levels levels = IntensityRescaling.computeLevels(DssScreeningUtils.createPixels(mergedImage), 30); int minLevel = levels.getMinLevel(); @@ -555,33 +515,37 @@ public class ImageChannelsUtils return mergedImage; } - private static IImageCalculator createCalculator(final ImageTransformationParams transformationInfo) + private static void calculateImagesForMerging(List<ImageWithReference> images, ImageTransformationParams transformationInfo) { - return new IImageCalculator() - { - @Override - public BufferedImage create(AbsoluteImageReference imageContent) - { - boolean applyNonImageLevelTransformation = false; - String transformationCode = null; - if (transformationInfo != null) - { - String channelCode = imageContent.tryGetChannelCode(); - transformationCode = transformationInfo.tryGetTransformationCodeForChannel(channelCode); - if (transformationCode != null) - { - applyNonImageLevelTransformation = transformationInfo.isApplyNonImageLevelTransformation(); - } - } - ImageTransformationParams info = new ImageTransformationParams(applyNonImageLevelTransformation, - false, transformationCode, null); - return calculateAndTransformSingleImageForDisplay(imageContent, info, null); - } - }; + for (ImageWithReference imageWithReference : images) + { + BufferedImage image = imageWithReference.getBufferedImage(); + AbsoluteImageReference imageReference = imageWithReference.getReference(); + image = rescaleIfNot8Bit(image, 0f); + image = resize(image, imageReference.getRequestedSize()); + image = transformForChannel(image, imageReference, transformationInfo); + image = transformGrayToColor(image, imageReference.getChannelColor()); + imageWithReference.setImage(image); + } + } + + private static BufferedImage transformForChannel(BufferedImage image, AbsoluteImageReference imageReference, + ImageTransformationParams transformationInfo) + { + String channelCode = imageReference.tryGetChannelCode(); + String transformationCode = transformationInfo.tryGetTransformationCodeForChannel(channelCode); + boolean applyNonImageLevelTransformation = false; + if (transformationCode != null) + { + applyNonImageLevelTransformation = transformationInfo.isApplyNonImageLevelTransformation(); + } + ImageTransformationParams info + = new ImageTransformationParams(applyNonImageLevelTransformation, false, transformationCode, null); + return transform(image, imageReference, info, 0f); } private static BufferedImage transform(BufferedImage image, - AbsoluteImageReference imageReference, ImageTransformationParams transformationInfo) + AbsoluteImageReference imageReference, ImageTransformationParams transformationInfo, Float threshold) { BufferedImage resultImage = image; ImageTransfomationFactories transformations = imageReference.getImageTransformationFactories(); @@ -615,7 +579,7 @@ public class ImageChannelsUtils if (channelLevelTransformationOrNull == null) { channelLevelTransformationOrNull = new AutoRescaleIntensityImageTransformerFactory( - ImageUtil.DEFAULT_IMAGE_OPTIMAL_RESCALING_FACTOR); + getThresholdValue(threshold)); } } return applyImageTransformation(resultImage, channelLevelTransformationOrNull); @@ -629,8 +593,8 @@ public class ImageChannelsUtils return image; } IImageTransformer transformer = transformerFactoryOrNull.createTransformer(); - BufferedImage transformImage = transformer.transform(image); - return transformImage; + BufferedImage transformedImage = transformer.transform(image); + return transformedImage; } private static List<ImageWithReference> calculateSingleImages(List<AbsoluteImageReference> imageContents, @@ -771,8 +735,12 @@ public class ImageChannelsUtils /** * Transforms the given <var>bufferedImage</var> by selecting a single channel from it. */ - public static BufferedImage transformToChannel(BufferedImage bufferedImage, final ColorComponent colorComponent) + public static BufferedImage extractChannel(BufferedImage bufferedImage, final ColorComponent colorComponent) { + if (colorComponent == null) + { + return bufferedImage; + } return transformColor(bufferedImage, new IColorTransformation() { @Override 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 02fb8c36342a62f8629a3713d1bdd5eec1cc47c1..f1e6389489e7becbdb1196b3406dc2b5e39ab9f7 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 @@ -25,6 +25,7 @@ import loci.formats.gui.Index16ColorModel; import net.lemnik.eodsql.QueryTool; import ch.systemsx.cisd.base.image.IImageTransformerFactory; +import ch.systemsx.cisd.common.image.IntensityRescaling.IImageToPixelsConverter; 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; @@ -39,6 +40,15 @@ import ch.systemsx.cisd.openbis.plugin.screening.shared.imaging.dataaccess.Trans */ public class DssScreeningUtils { + public static final IImageToPixelsConverter CONVERTER = new IImageToPixelsConverter() + { + @Override + public Pixels convert(BufferedImage image) + { + return createPixels(image); + } + }; + private static IImagingReadonlyQueryDAO query; static @@ -82,7 +92,7 @@ public class DssScreeningUtils ScreeningConstants.IMAGING_DATA_SOURCE); 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 @@ -116,5 +126,4 @@ public class DssScreeningUtils } }; } - } diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopySingleDatasetConfig.py b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopySingleDatasetConfig.py index 9f7a3988de02caac79e3e6475e950f6a6b63b200..149865111cdd22577b48258d1414624cf5097a1c 100644 --- a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopySingleDatasetConfig.py +++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopySingleDatasetConfig.py @@ -49,6 +49,8 @@ class MicroscopySingleDatasetConfig(SimpleImageContainerDataConfig): # Disable thumbnail generation by ImageMagick self.setUseImageMagicToGenerateThumbnails(False) + + self.setGenerateHighQuality8BitThumbnails(True) # Enable thumbnail generation self.setGenerateThumbnails(True) @@ -89,7 +91,7 @@ class MicroscopySingleDatasetConfig(SimpleImageContainerDataConfig): name = "No name" # Return the color - return Channel(channelCode, name) + return Channel(channelCode, name, self._getChannelColorRGB(channelCode)) def extractImagesMetadata(self, imagePath, imageIdentifiers): @@ -143,7 +145,7 @@ class MicroscopySingleDatasetConfig(SimpleImageContainerDataConfig): return metaData - def getChannelColorRGB(self, channelCode): + def _getChannelColorRGB(self, channelCode): """Returns a ChannelColorRGB instantiated with the RGB color components extracted from the file by the MetadataReader. diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/MicroscopyImageDropboxTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/MicroscopyImageDropboxTest.java index 132e6918a6b1acdb14c489a95c84d7479909d028..7c3cbd869e617a5606e1801dd7fbb2b979d6d6dd 100644 --- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/MicroscopyImageDropboxTest.java +++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/MicroscopyImageDropboxTest.java @@ -98,6 +98,15 @@ public class MicroscopyImageDropboxTest extends AbstractImageDropboxTestCase imageChecker.check(new File(getTestDataFolder(), "C1_512x512.png"), new ImageLoader(dataSet, sessionToken).microscopy().channel("SERIES-0_CHANNEL-1") .mode("thumbnail512x512")); + imageChecker.check(new File(getTestDataFolder(), "C2_Default.png"), + new ImageLoader(dataSet, sessionToken).microscopy().channel("SERIES-0_CHANNEL-2") + .mode("thumbnail480x480")); + imageChecker.check(new File(getTestDataFolder(), "C2_256x256.png"), + new ImageLoader(dataSet, sessionToken).microscopy().channel("SERIES-0_CHANNEL-2") + .mode("thumbnail256x256")); + imageChecker.check(new File(getTestDataFolder(), "C2_512x512.png"), + new ImageLoader(dataSet, sessionToken).microscopy().channel("SERIES-0_CHANNEL-2") + .mode("thumbnail512x512")); imageChecker.check(new File(getTestDataFolder(), "C01_Default.png"), new ImageLoader(dataSet, sessionToken).microscopy().channel("SERIES-0_CHANNEL-0") .channel("SERIES-0_CHANNEL-1").mode("thumbnail480x480")); @@ -107,9 +116,9 @@ public class MicroscopyImageDropboxTest extends AbstractImageDropboxTestCase imageChecker.check(new File(getTestDataFolder(), "C01_512x512.png"), new ImageLoader(dataSet, sessionToken).microscopy().channel("SERIES-0_CHANNEL-0") .channel("SERIES-0_CHANNEL-1").mode("thumbnail512x512")); - imageChecker.check(new File(getTestDataFolder(), "Merged_256x256_C0_0_20_C4_2_15.png"), - new ImageLoader(dataSet, sessionToken).microscopy().rescaling("SERIES-0_CHANNEL-0", 0, 20) - .rescaling("SERIES-0_CHANNEL-4", 2, 15).mode("thumbnail256x256")); + imageChecker.check(new File(getTestDataFolder(), "Merged_256x256_C0_0_200_C4_150_300.png"), + new ImageLoader(dataSet, sessionToken).microscopy().rescaling("SERIES-0_CHANNEL-0", 0, 200) + .rescaling("SERIES-0_CHANNEL-4", 150, 300).mode("thumbnail256x256")); imageChecker.assertNoFailures(); }