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 9cb341b526d8501b6393265c84510eea21ba120b..7e90bc48f80298bc4e9500f79810d9895ff77bf2 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 @@ -24,11 +24,15 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import javax.imageio.ImageIO; @@ -40,11 +44,14 @@ import ar.com.hjg.pngj.ImageInfo; import ar.com.hjg.pngj.ImageLine; import ar.com.hjg.pngj.PngFilterType; import ar.com.hjg.pngj.PngWriter; + import ch.rinn.restrictions.Private; import ch.systemsx.cisd.base.exceptions.IOExceptionUnchecked; import ch.systemsx.cisd.base.io.IRandomAccessFile; 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.GrayscalePixels; import ch.systemsx.cisd.common.image.IntensityRescaling.Levels; import ch.systemsx.cisd.common.logging.LogCategory; @@ -931,6 +938,49 @@ public class ImageUtil { return image.getColorModel().getColorSpace().getNumComponents() == 1; } + + /** + * Checks whether the specified image is actually a (colored) gray image. + * + * @return the representative color channel which can be used to extract the gray image. + * <code>null</code> if the image isn't a gray image. + */ + public static Channel getRepresentativeChannelIfEffectiveGray(BufferedImage image) + { + ImageHistogram imageHistogram = ImageHistogram.calculateHistogram(image); + Map<Channel, int[]> histogramsByChannels = new LinkedHashMap<Channel, int[]>(); + int numberOfPixels = image.getWidth() * image.getHeight(); + 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()) + { + 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(); + for (int i = 1; i < usedChannels.size(); i++) + { + int[] histogram = usedChannels.get(i).getValue(); + for (int j = 0; j < histogram.length; j++) + { + if (histogram[j] != representativeHistogram[j]) + { + return null; + } + } + } + return representativeChannel.getKey(); + } + + private static void checkIfChannelIsUsed(Map<Channel, int[]> usedChannels, int numberOfPixels, Channel channel, int[] histogram) + { + if (histogram[0] < numberOfPixels) + { + usedChannels.put(channel, histogram); + } + } /** * Re-scales the image to be the biggest one which fits into a (0,0,maxWidth, maxHeight) @@ -1011,9 +1061,6 @@ public class ImageUtil imageType = imageType == BufferedImage.TYPE_USHORT_GRAY ? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_INT_RGB; - } else if (imageType == BufferedImage.TYPE_CUSTOM) - { - imageType = isTransparent ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB; } else if (imageType == BufferedImage.TYPE_BYTE_INDEXED) { imageType = 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 d0952b083f7faac854f6555a114be10908d968c0..16f8790de14065579de07ca0a400bf3417f67da2 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 @@ -16,6 +16,9 @@ package ch.systemsx.cisd.openbis.dss.generic.shared.utils; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.File; import java.io.InputStream; @@ -30,6 +33,7 @@ import ch.systemsx.cisd.base.exceptions.IOExceptionUnchecked; import ch.systemsx.cisd.base.io.ByteBufferRandomAccessFile; import ch.systemsx.cisd.base.io.IRandomAccessFile; import ch.systemsx.cisd.base.io.RandomAccessFileImpl; +import ch.systemsx.cisd.common.image.IntensityRescaling.Channel; import ch.systemsx.cisd.imagereaders.ImageReaderConstants; import ch.systemsx.cisd.imagereaders.ImageReadersTestHelper; import ch.systemsx.cisd.openbis.common.io.FileBasedContentNode; @@ -304,6 +308,74 @@ public class ImageUtilTest extends AssertJUnit assertEquals(0, buffer.getFilePointer()); } + @Test + public void testGetRepresentaticChannelIfBlack() + { + BufferedImage image = new BufferedImage(20, 10, BufferedImage.TYPE_INT_RGB); + + Channel channel = ImageUtil.getRepresentativeChannelIfEffectiveGray(image); + + assertEquals(Channel.RED, channel); + } + + @Test + public void testGetRepresentaticChannelIfReallyGray() + { + BufferedImage image = new BufferedImage(20, 10, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = (Graphics2D) image.getGraphics(); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setColor(new Color(100, 100, 100)); + graphics.drawOval(0, 0, 5, 7); + + Channel channel = ImageUtil.getRepresentativeChannelIfEffectiveGray(image); + + assertEquals(Channel.RED, channel); + } + + @Test + public void testGetRepresentaticChannelIfColoredGrayGreen() + { + BufferedImage image = new BufferedImage(20, 10, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = (Graphics2D) image.getGraphics(); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setColor(new Color(0, 255, 0)); + graphics.drawOval(0, 0, 5, 7); + + Channel channel = ImageUtil.getRepresentativeChannelIfEffectiveGray(image); + + assertEquals(Channel.GREEN, channel); + } + + @Test + public void testGetRepresentaticChannelIfColoredGrayGreenAndBlue() + { + BufferedImage image = new BufferedImage(20, 10, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = (Graphics2D) image.getGraphics(); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setColor(new Color(0, 255, 255)); + graphics.drawOval(0, 0, 5, 7); + + Channel channel = ImageUtil.getRepresentativeChannelIfEffectiveGray(image); + + assertEquals(Channel.GREEN, channel); + } + + @Test + public void testGetRepresentaticChannelIfColored() + { + BufferedImage image = new BufferedImage(20, 10, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = (Graphics2D) image.getGraphics(); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setColor(Color.GREEN); + graphics.drawOval(0, 0, 5, 7); + graphics.setColor(Color.BLUE); + graphics.fillOval(3, 3, 3, 4); + + Channel channel = ImageUtil.getRepresentativeChannelIfEffectiveGray(image); + + assertEquals(null, channel); + } + private void assertFileType(String expectedFileType, String fileName) throws Exception { RandomAccessFileImpl handle = null; 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 new file mode 100644 index 0000000000000000000000000000000000000000..81c735e05c9337d3566b0cacd4410009a4df5fc2 Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256_C0_0_20_C4_2_15.png differ diff --git a/screening/resource/test-data/SimpleImageDropboxTest/1_1_DAPI_Default.png b/screening/resource/test-data/SimpleImageDropboxTest/1_1_DAPI_Default.png index 38e8bbd20a411bcd2436fdc3c41b075f920bebb8..5eaeb36b545a0f6aa138620126b2434779ad6e42 100644 Binary files a/screening/resource/test-data/SimpleImageDropboxTest/1_1_DAPI_Default.png and b/screening/resource/test-data/SimpleImageDropboxTest/1_1_DAPI_Default.png differ diff --git a/screening/resource/test-data/SimpleImageDropboxTest/1_1_Merged_Default.png b/screening/resource/test-data/SimpleImageDropboxTest/1_1_Merged_Default.png index 45479ae5be23afa3bf347c4bfc22efccbf52b3c5..5ef6b073c71901c960688be344ece94bf707ce4c 100644 Binary files a/screening/resource/test-data/SimpleImageDropboxTest/1_1_Merged_Default.png and b/screening/resource/test-data/SimpleImageDropboxTest/1_1_Merged_Default.png differ diff --git a/screening/resource/test-data/SimpleImageDropboxTest/1_1_Merged_Default_GFP_0_100.png b/screening/resource/test-data/SimpleImageDropboxTest/1_1_Merged_Default_GFP_0_100.png new file mode 100644 index 0000000000000000000000000000000000000000..68655e43c38560c42ecd17c8beaec838498e1414 Binary files /dev/null and b/screening/resource/test-data/SimpleImageDropboxTest/1_1_Merged_Default_GFP_0_100.png differ diff --git a/screening/resource/test-data/SimpleImageDropboxTest/1_2_Merged_1392x1040.png b/screening/resource/test-data/SimpleImageDropboxTest/1_2_Merged_1392x1040.png new file mode 100644 index 0000000000000000000000000000000000000000..ae35bbb850ad3cc6598e9c5a5d95b2032329a8c8 Binary files /dev/null and b/screening/resource/test-data/SimpleImageDropboxTest/1_2_Merged_1392x1040.png differ diff --git a/screening/resource/test-data/SimpleImageDropboxTest/1_3_DAPI_CY3_256x191.png b/screening/resource/test-data/SimpleImageDropboxTest/1_3_DAPI_CY3_256x191.png index 706b65693c749749475aec9ed869c846d3eb2b60..1dbe050dae9b9b58aa0d245179754120294f07fc 100644 Binary files a/screening/resource/test-data/SimpleImageDropboxTest/1_3_DAPI_CY3_256x191.png and b/screening/resource/test-data/SimpleImageDropboxTest/1_3_DAPI_CY3_256x191.png differ diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageDatasetLoaderFactory.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageDatasetLoaderFactory.java index a8eda14b07185f0087b4fbe6fcea5250e9523b0d..0b4f1d7c7647afca8ec415e4a2613504f9ca3dba 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageDatasetLoaderFactory.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageDatasetLoaderFactory.java @@ -35,4 +35,15 @@ public class HCSImageDatasetLoaderFactory { return ImagingDatasetLoader.tryCreate(DssScreeningUtils.getQuery(), datasetCode, content); } + + public static IImagingDatasetLoader create(IHierarchicalContent content, String datasetCode) + { + IImagingDatasetLoader loader = tryCreate(content, datasetCode); + if (loader == null) + { + throw new IllegalStateException(String.format( + "Dataset '%s' not found in the imaging database.", datasetCode)); + } + return loader; + } } diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/Utils.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/Utils.java index ae7cf5806e370c7d8e7316e5ddb7a27939b2f361..af991b3333fcd68f6f3f534d3939759f40bc700f 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/Utils.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/Utils.java @@ -49,8 +49,7 @@ public class Utils } BufferedImage image = ImageUtil.loadUnchangedImage(contentNode, imageIdOrNull, imageLibraryNameOrNull, imageLibraryReaderNameOrNull, null); - - return ImageUtil.convertToRGB(image); + return image; } public static Size loadUnchangedImageSize(IHierarchicalContentNode contentNode, 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 125699044527f5ad28b232f50bb9c26044dc0bda..94df6986d01827c64a4902e48eb7b0a5699f8e26 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 @@ -18,14 +18,18 @@ package ch.systemsx.cisd.openbis.dss.etl.dto.api.transformations; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; +import java.util.Arrays; import java.util.EnumSet; 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.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.test.ImageDebugViewer; +import ch.systemsx.cisd.openbis.dss.generic.shared.utils.ImageUtil; /** * Transformation performed by @@ -60,22 +64,17 @@ public class AutoRescaleIntensityImageTransformerFactory implements IImageTransf { if (IntensityRescaling.isNotGrayscale(image)) { - EnumSet<Channel> channels = IntensityRescaling.getUsedRgbChannels(image); - if (channels.size() != 1) + Channel channel = ImageUtil.getRepresentativeChannelIfEffectiveGray(image); + if (channel == null) { return image; - } else - { - Channel channel = channels.iterator().next(); - Levels levels = - IntensityRescaling.computeLevels(toGrayScale(image, channel), - threshold); - return IntensityRescaling.rescaleIntensityLevelTo8Bits(image, levels, - channel); } + BufferedImage grayScaleImage = toGrayScale(image, channel); + Levels levels = IntensityRescaling.computeLevels(grayScaleImage, threshold); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(image, levels); } Levels levels = IntensityRescaling.computeLevels(image, threshold); - return IntensityRescaling.rescaleIntensityLevelTo8Bits(image, levels); + return IntensityRescaling.rescaleIntensityLevelTo8Bits(new IntensityRescaling.GrayscalePixels(image), levels); } }; } 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 adc2c89882cf2a47d45851cebe5899601281f1d2..c81abae54547ebb0ddbfaa214be0d0b2b7c134b1 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 @@ -19,10 +19,8 @@ package ch.systemsx.cisd.openbis.dss.generic.server.images; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics2D; -import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; -import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -31,10 +29,11 @@ import java.util.Map; import org.apache.log4j.Logger; 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.EnvironmentFailureException; import ch.systemsx.cisd.common.exceptions.UserFailureException; -import ch.systemsx.cisd.common.image.IntensityRescaling; +import ch.systemsx.cisd.common.image.ImageHistogram; +import ch.systemsx.cisd.common.image.IntensityRescaling.Channel; import ch.systemsx.cisd.common.image.MixColors; import ch.systemsx.cisd.common.image.MixColors.MixedImageWithWhitePoint; import ch.systemsx.cisd.common.logging.LogCategory; @@ -80,34 +79,43 @@ public class ImageChannelsUtils // MIME type of the images which are produced by this class public static final String IMAGES_CONTENT_TYPE = "image/png"; + + private static interface IColorTransformation + { + int transform(int rgb); + } - public static interface IDatasetDirectoryProvider + private static interface IImageCalculator { - /** directory where dataset can be found in DSS store */ - File getDatasetRoot(String datasetCode); + public BufferedImage create(AbsoluteImageReference imageContent); } - private final IImagingLoaderStrategy imageLoaderStrategy; + private static class ImageWithReference + { + private BufferedImage image; - private final RequestedImageSize imageSizeLimit; + private final AbsoluteImageReference reference; - private final String singleChannelTransformationCodeOrNull; + public ImageWithReference(BufferedImage image, AbsoluteImageReference reference) + { + this.image = image; + this.reference = reference; + } - @Private - ImageChannelsUtils(IImagingLoaderStrategy imageLoaderStrategy, - RequestedImageSize imageSizeLimit, String singleChannelTransformationCodeOrNull) - { - this.imageLoaderStrategy = imageLoaderStrategy; - this.imageSizeLimit = imageSizeLimit; - this.singleChannelTransformationCodeOrNull = singleChannelTransformationCodeOrNull; - } + public BufferedImage getBufferedImage() + { + return image; + } + + public void setImage(BufferedImage image) + { + this.image = image; + } - @Private - ImageChannelsUtils(IImagingLoaderStrategy imageLoaderStrategy, Size imageSizeLimitOrNull, - String singleChannelTransformationCodeOrNull) - { - this(imageLoaderStrategy, new RequestedImageSize(imageSizeLimitOrNull, false), - singleChannelTransformationCodeOrNull); + public AbsoluteImageReference getReference() + { + return reference; + } } /** @@ -147,7 +155,7 @@ public class ImageChannelsUtils /** * Returns content of image for the specified tile in the specified size and for the requested - * channel or with all channels merged. + * channel or with all channels merged. This method is called by the servlets which delivers images. * * @param params * @param contentProvider @@ -155,8 +163,6 @@ public class ImageChannelsUtils public static ResponseContentStream getImageStream(ImageGenerationDescription params, IHierarchicalContentProvider contentProvider) { - Size thumbnailSizeOrNull = params.tryGetThumbnailSize(); - ImageRepresentationFormat existingRepresentationFormat = RepresentationUtil.tryGetRepresentationFormat(params); @@ -167,45 +173,63 @@ public class ImageChannelsUtils tryGetRawContentOfExistingThumbnail(params, existingRepresentationFormat); if (content != null) { - return asResponseContentStream(content); +// return asResponseContentStream(content); } } - BufferedImage image = null; + BufferedImage image = calculateImage(params, contentProvider); + image = drawOverlays(image, params, contentProvider); + if (image == null) + { + throw new UserFailureException("No image is available for parameters: " + params); + } + return createResponseContentStream(image, null); + } + + private static BufferedImage calculateImage(ImageGenerationDescription params, + IHierarchicalContentProvider contentProvider) + { DatasetAcquiredImagesReference imageChannels = params.tryGetImageChannels(); - if (imageChannels != null) + if (imageChannels == null) { - RequestedImageSize imageSize = new RequestedImageSize(thumbnailSizeOrNull, false); - image = - calculateBufferedImage(imageChannels, - params.tryGetSingleChannelTransformationCode(), - params.tryGetTransformationsPerChannel(), contentProvider, imageSize); + return null; } + RequestedImageSize imageSize = new RequestedImageSize(params.tryGetThumbnailSize(), false); + String transformationCode = params.tryGetSingleChannelTransformationCode(); + Map<String, String> transformationsPerChannel = params.tryGetTransformationsPerChannel(); + ImageLoadingHelper imageLoadingHelper = new ImageLoadingHelper(imageChannels, contentProvider, imageSize, transformationCode); + boolean mergeAllChannels = imageLoadingHelper.isMergeAllChannels(imageChannels); + ImageTransformationParams transformationInfo = new ImageTransformationParams(true, mergeAllChannels, + transformationCode, transformationsPerChannel); + List<AbsoluteImageReference> imageContents = + imageLoadingHelper.fetchImageContents(imageChannels, mergeAllChannels, false, transformationInfo); + return calculateBufferedImage(imageContents, transformationInfo); + } - RequestedImageSize overlaySize = calcOverlaySize(image, thumbnailSizeOrNull); + private static BufferedImage drawOverlays(BufferedImage imageOrNull, ImageGenerationDescription params, + IHierarchicalContentProvider contentProvider) + { + RequestedImageSize overlaySize = calcOverlaySize(imageOrNull, params.tryGetThumbnailSize()); + BufferedImage imageWithOverlays = imageOrNull; for (DatasetAcquiredImagesReference overlayChannels : params.getOverlayChannels()) { // NOTE: never merges the overlays, draws each channel separately (merging looses // transparency and is slower) - List<ImageWithReference> overlayImages = - getSingleImagesSkipNonExisting(overlayChannels, overlaySize, - params.tryGetSingleChannelTransformationCode(), contentProvider); + String transformationCode = params.tryGetSingleChannelTransformationCode(); + List<ImageWithReference> overlayImages = getSingleImagesSkipNonExisting(overlayChannels, + overlaySize, transformationCode, contentProvider); for (ImageWithReference overlayImage : overlayImages) { - if (image != null) + if (imageWithOverlays != null) { - drawOverlay(image, overlayImage); + drawOverlay(imageWithOverlays, overlayImage); } else { - image = overlayImage.getBufferedImage(); + imageWithOverlays = overlayImage.getBufferedImage(); } } } - if (image == null) - { - throw new UserFailureException("No image is available for parameters: " + params); - } - return createResponseContentStream(image, null); + return imageWithOverlays; } private static List<ImageWithReference> getSingleImagesSkipNonExisting( @@ -213,17 +237,22 @@ public class ImageChannelsUtils String singleChannelTransformationCodeOrNull, IHierarchicalContentProvider contentProvider) { - ImageChannelsUtils utils = - createImageChannelsUtils(imagesReference, contentProvider, imageSize, - singleChannelTransformationCodeOrNull); - boolean mergeAllChannels = utils.isMergeAllChannels(imagesReference); - ImageTransformationParams transformationInfo = - new ImageTransformationParams(true, mergeAllChannels, null, - new HashMap<String, String>()); + ImageLoadingHelper imageLoadingHelper = new ImageLoadingHelper(imagesReference, + contentProvider, imageSize, singleChannelTransformationCodeOrNull); + boolean mergeAllChannels = imageLoadingHelper.isMergeAllChannels(imagesReference); + final ImageTransformationParams transformationInfo = + new ImageTransformationParams(true, mergeAllChannels, null, new HashMap<String, String>()); List<AbsoluteImageReference> imageContents = - utils.fetchImageContents(imagesReference, mergeAllChannels, true, + imageLoadingHelper.fetchImageContents(imagesReference, mergeAllChannels, true, transformationInfo); - return calculateSingleImagesForDisplay(imageContents, transformationInfo, 0.0f, null); + return calculateSingleImages(imageContents, new IImageCalculator() + { + @Override + public BufferedImage create(AbsoluteImageReference imageContent) + { + return calculateAndTransformSingleImageForDisplay(imageContent, transformationInfo, 0f); + } + }); } private static RequestedImageSize getSize(BufferedImage img, boolean highQuality) @@ -261,116 +290,6 @@ public class ImageChannelsUtils return asResponseContentStream(imageContent); } - private static BufferedImage calculateBufferedImage( - DatasetAcquiredImagesReference imageChannels, - String singleChannelTransformationCodeOrNull, - Map<String, String> transformationsPerChannels, - IHierarchicalContentProvider contentProvider, RequestedImageSize imageSizeLimit) - { - ImageChannelsUtils imageChannelsUtils = - createImageChannelsUtils(imageChannels, contentProvider, imageSizeLimit, - singleChannelTransformationCodeOrNull); - boolean useMergedChannelsTransformation = - imageChannelsUtils.isMergeAllChannels(imageChannels); - ImageTransformationParams transformationInfo = - new ImageTransformationParams(true, useMergedChannelsTransformation, - singleChannelTransformationCodeOrNull, transformationsPerChannels); - - return imageChannelsUtils.calculateBufferedImage(imageChannels, transformationInfo); - } - - private static ImageChannelsUtils createImageChannelsUtils( - DatasetAcquiredImagesReference imageChannels, - IHierarchicalContentProvider contentProvider, RequestedImageSize imageSizeLimit, - String singleChannelTransformationCodeOrNull) - { - IImagingDatasetLoader imageAccessor = createImageAccessor(imageChannels, contentProvider); - return new ImageChannelsUtils( - ImagingLoaderStrategyFactory.createImageLoaderStrategy(imageAccessor), - imageSizeLimit, singleChannelTransformationCodeOrNull); - } - - @Private - BufferedImage calculateBufferedImage(DatasetAcquiredImagesReference imageChannels, - ImageTransformationParams transformationInfo) - { - boolean mergeAllChannels = isMergeAllChannels(imageChannels); - List<AbsoluteImageReference> imageContents = - fetchImageContents(imageChannels, mergeAllChannels, false, transformationInfo); - return calculateBufferedImage(imageContents, transformationInfo); - } - - private boolean isMergeAllChannels(DatasetAcquiredImagesReference imageChannels) - { - return imageChannels.isMergeAllChannels(getAllChannelCodes()); - } - - /** - * @param skipNonExisting if true references to non-existing images are ignored, otherwise an - * exception is thrown - * @param mergeAllChannels true if all existing channel images should be merged - * @param transformationInfo - */ - private List<AbsoluteImageReference> fetchImageContents( - DatasetAcquiredImagesReference imagesReference, boolean mergeAllChannels, - boolean skipNonExisting, ImageTransformationParams transformationInfo) - { - List<String> channelCodes = imagesReference.getChannelCodes(getAllChannelCodes()); - List<AbsoluteImageReference> images = new ArrayList<AbsoluteImageReference>(); - for (String channelCode : channelCodes) - { - ImageChannelStackReference channelStackReference = - imagesReference.getChannelStackReference(); - AbsoluteImageReference image = - imageLoaderStrategy.tryGetImage(channelCode, channelStackReference, - imageSizeLimit, singleChannelTransformationCodeOrNull); - if (image == null && skipNonExisting == false) - { - throw createImageNotFoundException(channelStackReference, channelCode); - } - if (image != null) - { - images.add(image); - } - } - - // Optimization for a case where all channels are on one image - if (mergeAllChannels - && (false == shouldApplySingleChannelsTransformations(transformationInfo))) - { - AbsoluteImageReference allChannelsImageReference = - tryCreateAllChannelsImageReference(images); - if (allChannelsImageReference != null) - { - images.clear(); - images.add(allChannelsImageReference); - } - } - return images; - } - - private boolean shouldApplySingleChannelsTransformations( - ImageTransformationParams transformationInfo) - { - if (transformationInfo == null - || transformationInfo.tryGetTransformationCodeForChannels() == null - || transformationInfo.tryGetTransformationCodeForChannels().size() == 0) - { - return false; - } - - return true; - } - - private static IImagingDatasetLoader createImageAccessor( - DatasetAcquiredImagesReference imagesReference, - IHierarchicalContentProvider contentProvider) - { - String datasetCode = imagesReference.getDatasetCode(); - IHierarchicalContent dataSetRoot = contentProvider.asContent(datasetCode); - return createDatasetLoader(dataSetRoot, datasetCode); - } - /** * Returns content of the image which is representative for the given dataset. */ @@ -378,32 +297,18 @@ public class ImageChannelsUtils IHierarchicalContent dataSetRoot, String datasetCode, Location wellLocationOrNull, Size imageSizeLimitOrNull, String singleChannelTransformationCodeOrNull) { - IImagingDatasetLoader imageAccessor = createDatasetLoader(dataSetRoot, datasetCode); - List<AbsoluteImageReference> imageReferences = - new ImageChannelsUtils( - ImagingLoaderStrategyFactory.createImageLoaderStrategy(imageAccessor), - imageSizeLimitOrNull, singleChannelTransformationCodeOrNull) - .getRepresentativeImageReferences(wellLocationOrNull); - BufferedImage image = - calculateBufferedImage(imageReferences, new ImageTransformationParams(true, true, - null, new HashMap<String, String>())); + IImagingDatasetLoader imageAccessor = HCSImageDatasetLoaderFactory.create(dataSetRoot, datasetCode); + IImagingLoaderStrategy loaderStrategy = ImagingLoaderStrategyFactory.createImageLoaderStrategy(imageAccessor); + ImageLoadingHelper imageLoadingHelper = + new ImageLoadingHelper(loaderStrategy, imageSizeLimitOrNull, singleChannelTransformationCodeOrNull); + List<AbsoluteImageReference> imageReferences = imageLoadingHelper.getRepresentativeImageReferences(wellLocationOrNull); + ImageTransformationParams transformationParams = new ImageTransformationParams(true, true, + null, new HashMap<String, String>()); + BufferedImage image = calculateBufferedImage(imageReferences, transformationParams); String name = createFileName(datasetCode, wellLocationOrNull, imageSizeLimitOrNull); return createResponseContentStream(image, name); } - private static IImagingDatasetLoader createDatasetLoader(IHierarchicalContent dataSetRoot, - String datasetCode) - { - IImagingDatasetLoader loader = - HCSImageDatasetLoaderFactory.tryCreate(dataSetRoot, datasetCode); - if (loader == null) - { - throw new IllegalStateException(String.format( - "Dataset '%s' not found in the imaging database.", datasetCode)); - } - return loader; - } - private static String createFileName(String datasetCode, Location wellLocationOrNull, Size imageSizeLimitOrNull) { @@ -442,16 +347,16 @@ public class ImageChannelsUtils chosenChannelCode == null ? ScreeningConstants.MERGED_CHANNELS : chosenChannelCode); - ImageChannelsUtils imageChannelsUtils = - new ImageChannelsUtils(imageLoaderStrategy, imageSizeLimitOrNull, + ImageLoadingHelper imageLoadingHelper = + new ImageLoadingHelper(imageLoaderStrategy, imageSizeLimitOrNull, singleChannelImageTransformationCodeOrNull); - boolean mergeAllChannels = imageChannelsUtils.isMergeAllChannels(imagesReference); + boolean mergeAllChannels = imageLoadingHelper.isMergeAllChannels(imagesReference); ImageTransformationParams transformationInfo = new ImageTransformationParams(transform, mergeAllChannels, singleChannelImageTransformationCodeOrNull, new HashMap<String, String>()); List<AbsoluteImageReference> imageContents = - imageChannelsUtils.fetchImageContents(imagesReference, mergeAllChannels, false, + imageLoadingHelper.fetchImageContents(imagesReference, mergeAllChannels, false, transformationInfo); IHierarchicalContentNode contentNode = tryGetRawContent(convertToPng, imageContents); @@ -493,47 +398,6 @@ public class ImageChannelsUtils } return null; } - - private List<AbsoluteImageReference> getRepresentativeImageReferences( - Location wellLocationOrNull) - { - List<AbsoluteImageReference> images = new ArrayList<AbsoluteImageReference>(); - - for (String chosenChannel : getAllChannelCodes()) - { - AbsoluteImageReference image = - getRepresentativeImageReference(chosenChannel, wellLocationOrNull); - images.add(image); - } - return images; - } - - private List<String> getAllChannelCodes() - { - return imageLoaderStrategy.getImageParameters().getChannelsCodes(); - } - - /** - * @throw {@link EnvironmentFailureException} when image does not exist - */ - private AbsoluteImageReference getRepresentativeImageReference(String channelCode, - Location wellLocationOrNull) - { - AbsoluteImageReference image = - imageLoaderStrategy.tryGetRepresentativeImage(channelCode, wellLocationOrNull, - imageSizeLimit, singleChannelTransformationCodeOrNull); - if (image != null) - { - return image; - } else - { - throw EnvironmentFailureException.fromTemplate( - "No representative " - + (imageSizeLimit.isThumbnailRequired() ? "thumbnail" : "image") - + " found for well %s and channel %s", wellLocationOrNull, channelCode); - } - } - /** * @param threshold * @param useMergedChannelsTransformation sometimes we can have a single image which contain all @@ -546,13 +410,20 @@ public class ImageChannelsUtils BufferedImage image = calculateSingleImage(imageReference); image = transform(image, imageReference, transformationInfo); image = ImageUtil.convertForDisplayIfNecessary(image, threshold); + Channel channel = ImageUtil.getRepresentativeChannelIfEffectiveGray(image); + if (channel != null) + { + final ChannelColorRGB channelColor = imageReference.getChannelColor(); + image = transformColor(image, createColorTransformation(channel, channelColor)); + } 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)); @@ -565,14 +436,14 @@ public class ImageChannelsUtils if (size != null) { start = operationLog.isDebugEnabled() ? System.currentTimeMillis() : 0; - image = - ImageUtil.rescale(image, size.getWidth(), size.getHeight(), - requestedSize.enlargeIfNecessary(), - requestedSize.isHighQualityRescalingRequired()); + boolean enlarge = requestedSize.enlargeIfNecessary(); + boolean highQuality8Bit = requestedSize.isHighQualityRescalingRequired(); + image = ImageUtil.rescale(image, size.getWidth(), size.getHeight(), enlarge, highQuality8Bit); if (operationLog.isDebugEnabled()) { operationLog.debug("Create thumbnail: " + (System.currentTimeMillis() - start)); } + } // choose color component if necessary @@ -590,25 +461,41 @@ public class ImageChannelsUtils return image; } - /** - * @param allChannelsMerged if true then we use one special transformation on the merged images - * instead of transforming every single image. - */ - private static BufferedImage calculateBufferedImage( - List<AbsoluteImageReference> imageReferences, + private static IColorTransformation createColorTransformation(final Channel channel, final ChannelColorRGB channelColor) + { + return new IColorTransformation() + { + + @Override + public int transform(int rgb) + { + int gray = (rgb >> channel.getShift()) & 0xff; + int red = scale(gray, channelColor.getR()); + int green = scale(gray, channelColor.getG()); + int blue = scale(gray, channelColor.getB()); + return (red << 16) + (green << 8) + blue; + } + + private int scale(int value, int maxValue) + { + return maxValue == 0 ? 0 : Math.min(255, (value * 255) / maxValue); + } + }; + } + + @Private + static BufferedImage calculateBufferedImage(List<AbsoluteImageReference> imageReferences, ImageTransformationParams transformationInfo) { AbsoluteImageReference singleImageReference = imageReferences.get(0); if (imageReferences.size() == 1) { - return calculateAndTransformSingleImageForDisplay(singleImageReference, - transformationInfo, null); + return calculateAndTransformSingleImageForDisplay(singleImageReference, transformationInfo, null); } else { IImageTransformerFactory mergedChannelTransformationOrNull = - singleImageReference.getImageTransfomationFactories().tryGetForMerged(); - return mergeChannels(imageReferences, transformationInfo, - mergedChannelTransformationOrNull); + singleImageReference.getImageTransformationFactories().tryGetForMerged(); + return mergeChannels(imageReferences, transformationInfo, mergedChannelTransformationOrNull); } } @@ -618,38 +505,85 @@ public class ImageChannelsUtils { // We do not transform single images here. List<ImageWithReference> images = - calculateSingleImagesForDisplay(imageReferences, null, null, transformationInfo); + calculateSingleImages(imageReferences, createCalculator(transformationInfo)); + for (int i = 0; i < images.size(); i++) + { + 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); + } + } - MixedImageWithWhitePoint mergedImageWithWhitePoint = mergeImages(images); - BufferedImage mergedImage = mergedImageWithWhitePoint.getImage(); + MixedImageWithWhitePoint mergedImageWithWhitePoint = mergeImages(images); + BufferedImage mergedImage = mergedImageWithWhitePoint.getImage(); - // non-user transformation - apply color range fix after mixing + // non-user transformation - apply color range fix after mixing Map<String, String> transMap = transformationInfo.tryGetTransformationCodeForChannels(); - Color whitePointColor = mergedImageWithWhitePoint.getWhitePoint(); - if ((transMap == null || transMap.size() == 0) && mergedChannelTransformationOrNull == null && whitePointColor != null) + Color whitePointColor = mergedImageWithWhitePoint.getWhitePoint(); + IImageTransformerFactory channelTransformation = mergedChannelTransformationOrNull; + if ((transMap == null || transMap.size() == 0) && channelTransformation == null && whitePointColor != null) { - mergedChannelTransformationOrNull = new IntensityRangeImageTransformerFactory(0, Math.max(Math.max(whitePointColor.getRed(), whitePointColor.getGreen()), whitePointColor.getBlue())); + int red = whitePointColor.getRed(); + int green = whitePointColor.getGreen(); + int blue = whitePointColor.getBlue(); + int whitePoint = Math.max(Math.max(red, green), blue); + channelTransformation = new IntensityRangeImageTransformerFactory(0, whitePoint); } // NOTE: even if we are not merging all the channels but just few of them we use the // merged-channel transformation if (transformationInfo.isApplyNonImageLevelTransformation()) { - mergedImage = applyImageTransformation(mergedImage, mergedChannelTransformationOrNull); + mergedImage = applyImageTransformation(mergedImage, channelTransformation); } return mergedImage; } + private static IImageCalculator createCalculator(final ImageTransformationParams transformationInfo) + { + return new IImageCalculator() + { + @Override + public BufferedImage create(AbsoluteImageReference imageContent) + { + String channelCode = imageContent.tryGetChannelCode(); + if (transformationInfo != null + && transformationInfo.tryGetTransformationCodeForChannel(channelCode) != null) + { + String transformationCode = + transformationInfo.tryGetTransformationCodeForChannel(channelCode); + boolean applyNonImageLevelTransformation = + transformationInfo.isApplyNonImageLevelTransformation(); + ImageTransformationParams info = + new ImageTransformationParams(applyNonImageLevelTransformation, false, + transformationCode, null); + return calculateAndTransformSingleImageForDisplay(imageContent, info, null); + } else + { + // NOTE: here we skip image level transformations as well + BufferedImage image = calculateSingleImage(imageContent); + BufferedImage image2 = ImageUtil.convertForDisplayIfNecessary(image, null); + return image2; + } + } + }; + } + private static BufferedImage transform(BufferedImage image, AbsoluteImageReference imageReference, ImageTransformationParams transformationInfo) { BufferedImage resultImage = image; - ImageTransfomationFactories transfomations = - imageReference.getImageTransfomationFactories(); + ImageTransfomationFactories transformations = imageReference.getImageTransformationFactories(); // image level transformation is applied always, as it cannot be applied or changed in // external image viewer - resultImage = applyImageTransformation(resultImage, transfomations.tryGetForImage()); + resultImage = applyImageTransformation(resultImage, transformations.tryGetForImage()); if (transformationInfo.isApplyNonImageLevelTransformation() == false) { @@ -658,28 +592,24 @@ public class ImageChannelsUtils IImageTransformerFactory channelLevelTransformationOrNull = null; if (transformationInfo.isUseMergedChannelsTransformation()) { - channelLevelTransformationOrNull = transfomations.tryGetForMerged(); + channelLevelTransformationOrNull = transformations.tryGetForMerged(); } else { - String channelTransformationCode = - transformationInfo.tryGetSingleChannelTransformationCode() == null ? transfomations - .tryGetDefaultTransformationCode() - : transformationInfo - .tryGetSingleChannelTransformationCode(); - + String transformationCode = transformationInfo.tryGetSingleChannelTransformationCode(); + String channelTransformationCode = transformationCode; + if (transformationCode == null) + { + channelTransformationCode = transformations.tryGetDefaultTransformationCode(); + } if (channelTransformationCode != null && (false == channelTransformationCode.equals(imageReference .tryGetSingleChannelTransformationCode()))) { - channelLevelTransformationOrNull = - transfomations.tryGetForChannel(transformationInfo - .tryGetSingleChannelTransformationCode()); + channelLevelTransformationOrNull = transformations.tryGetForChannel(transformationCode); } - if (channelLevelTransformationOrNull == null) { - channelLevelTransformationOrNull = - new AutoRescaleIntensityImageTransformerFactory( + channelLevelTransformationOrNull = new AutoRescaleIntensityImageTransformerFactory( ImageUtil.DEFAULT_IMAGE_OPTIMAL_RESCALING_FACTOR); } } @@ -694,109 +624,23 @@ public class ImageChannelsUtils { return image; } - return transformerFactoryOrNull.createTransformer().transform(image); + IImageTransformer transformer = transformerFactoryOrNull.createTransformer(); + BufferedImage transformImage = transformer.transform(image); + return transformImage; } - private static class ImageWithReference - { - private final BufferedImage image; - - private final AbsoluteImageReference reference; - - public ImageWithReference(BufferedImage image, AbsoluteImageReference reference) - { - this.image = image; - this.reference = reference; - } - - public BufferedImage getBufferedImage() - { - return image; - } - - public AbsoluteImageReference getReference() - { - return reference; - } - } - - /** - * @param transformationInfoOrNull if null all transformations (including image-level) will be - * skipped - * @param transformationInfo - */ - private static List<ImageWithReference> calculateSingleImagesForDisplay( - List<AbsoluteImageReference> imageReferences, - ImageTransformationParams transformationInfoOrNull, Float threshold, - ImageTransformationParams transformationInfoForMergingOrNull) + private static List<ImageWithReference> calculateSingleImages(List<AbsoluteImageReference> imageContents, + IImageCalculator imageCalculator) { List<ImageWithReference> images = new ArrayList<ImageWithReference>(); - for (AbsoluteImageReference imageRef : imageReferences) + for (AbsoluteImageReference imageContent : imageContents) { - BufferedImage image; - if (transformationInfoOrNull != null) - { - image = - calculateAndTransformSingleImageForDisplay(imageRef, - transformationInfoOrNull, threshold); - } else if (transformationInfoForMergingOrNull != null - && null != transformationInfoForMergingOrNull - .tryGetTransformationCodeForChannel(imageRef.tryGetChannelCode())) - { - String transformationCode = - transformationInfoForMergingOrNull - .tryGetTransformationCodeForChannel(imageRef.tryGetChannelCode()); - image = - calculateAndTransformSingleImageForDisplay( - imageRef, - new ImageTransformationParams(transformationInfoForMergingOrNull - .isApplyNonImageLevelTransformation(), false, - transformationCode, null), threshold); - } else - { - // NOTE: here we skip image level transformations as well - image = calculateSingleImage(imageRef); - image = ImageUtil.convertForDisplayIfNecessary(image, threshold); - } - images.add(new ImageWithReference(image, imageRef)); + BufferedImage image = imageCalculator.create(imageContent); + images.add(new ImageWithReference(image, imageContent)); } return images; } - - // Checks if all images differ only at the color component level and stem from the same page - // of the same file. If that's the case any image from the collection contains the merged - // channels image (if we erase the color component). - private static AbsoluteImageReference tryCreateAllChannelsImageReference( - List<AbsoluteImageReference> imageReferences) - { - AbsoluteImageReference lastFound = null; - for (AbsoluteImageReference image : imageReferences) - { - if (lastFound == null) - { - lastFound = image; - } else - { - if (equals(image.tryGetImageID(), lastFound.tryGetImageID()) == false - || image.getUniqueId().equals(lastFound.getUniqueId()) == false) - { - return null; - } - } - } - if (lastFound != null) - { - return lastFound.createWithoutColorComponent(); - } else - { - return null; - } - } - - private static boolean equals(String i1OrNull, String i2OrNull) - { - return (i1OrNull == null) ? (i2OrNull == null) : i1OrNull.equals(i2OrNull); - } + // this method always returns RGB images, even if the input was in grayscale private static MixedImageWithWhitePoint mergeImages(List<ImageWithReference> images) @@ -920,22 +764,24 @@ public class ImageChannelsUtils return image.getBufferedImage().getColorModel().hasAlpha(); } - // --------- common - - private EnvironmentFailureException createImageNotFoundException( - ImageChannelStackReference channelStackReference, String chosenChannelCode) - { - return EnvironmentFailureException.fromTemplate( - "No " + (imageSizeLimit.isThumbnailRequired() ? "thumbnail" : "image") - + " found for channel stack %s and channel %s", channelStackReference, - chosenChannelCode); - } - /** * Transforms the given <var>bufferedImage</var> by selecting a single channel from it. */ - public static BufferedImage transformToChannel(BufferedImage bufferedImage, - ColorComponent colorComponent) + public static BufferedImage transformToChannel(BufferedImage bufferedImage, final ColorComponent colorComponent) + { + return transformColor(bufferedImage, new IColorTransformation() + { + @Override + public int transform(int rgb) + { + // We reset all ingredients besides the one which is specified by color component. + // The result is the rgb value with only one component which is non-zero. + return colorComponent.extractSingleComponent(rgb).getRGB(); + } + }); + } + + public static BufferedImage transformColor(BufferedImage bufferedImage, IColorTransformation transformation) { BufferedImage newImage = createNewRGBImage(bufferedImage); int width = bufferedImage.getWidth(); @@ -945,20 +791,12 @@ public class ImageChannelsUtils for (int x = 0; x < width; x++) { int rgb = bufferedImage.getRGB(x, y); - int channelColor = extractSingleComponent(rgb, colorComponent); - newImage.setRGB(x, y, channelColor); + newImage.setRGB(x, y, transformation.transform(rgb)); } } return newImage; } - - // We reset all ingredients besides the one which is specified by color component. - // The result is the rgb value with only one component which is non-zero. - private static int extractSingleComponent(int rgb, ColorComponent colorComponent) - { - return colorComponent.extractSingleComponent(rgb).getRGB(); - } - + // NOTE: drawing on this image will not preserve transparency - but we do not need it and the // image is smaller private static BufferedImage createNewRGBImage(RenderedImage bufferedImage) @@ -975,4 +813,10 @@ public class ImageChannelsUtils return new ByteArrayBasedContentNode(output, nameOrNull); } + private static void logImage(String title, BufferedImage image) + { + ImageHistogram calculateHistogram = ImageHistogram.calculateHistogram(image); + System.err.println(title+": "+image+"\n" + calculateHistogram); + } + } diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageLoadingHelper.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageLoadingHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..2ed25e31b9420446c124bd81a0c34efff81bee79 --- /dev/null +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageLoadingHelper.java @@ -0,0 +1,203 @@ +/* + * Copyright 2014 ETH Zuerich, SIS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.openbis.dss.generic.server.images; + +import java.util.ArrayList; +import java.util.List; + +import ch.rinn.restrictions.Private; +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.hcs.Location; +import ch.systemsx.cisd.openbis.common.io.hierarchical_content.api.IHierarchicalContent; +import ch.systemsx.cisd.openbis.dss.etl.AbsoluteImageReference; +import ch.systemsx.cisd.openbis.dss.etl.HCSImageDatasetLoaderFactory; +import ch.systemsx.cisd.openbis.dss.etl.IImagingDatasetLoader; +import ch.systemsx.cisd.openbis.dss.etl.IImagingLoaderStrategy; +import ch.systemsx.cisd.openbis.dss.etl.ImagingLoaderStrategyFactory; +import ch.systemsx.cisd.openbis.dss.generic.server.images.dto.DatasetAcquiredImagesReference; +import ch.systemsx.cisd.openbis.dss.generic.server.images.dto.ImageChannelStackReference; +import ch.systemsx.cisd.openbis.dss.generic.server.images.dto.ImageTransformationParams; +import ch.systemsx.cisd.openbis.dss.generic.server.images.dto.RequestedImageSize; +import ch.systemsx.cisd.openbis.dss.generic.shared.IHierarchicalContentProvider; +import ch.systemsx.cisd.openbis.dss.generic.shared.dto.Size; + +/** + * Helper class for loading images using an {@link IImagingLoaderStrategy}. + * + * @author Franz-Josef Elmer + */ +class ImageLoadingHelper +{ + private static IImagingLoaderStrategy createLoaderStrategy(DatasetAcquiredImagesReference imageChannels, + IHierarchicalContentProvider contentProvider) + { + String datasetCode = imageChannels.getDatasetCode(); + IHierarchicalContent dataSetRoot = contentProvider.asContent(datasetCode); + IImagingDatasetLoader loader = HCSImageDatasetLoaderFactory.create(dataSetRoot, datasetCode); + return ImagingLoaderStrategyFactory.createImageLoaderStrategy(loader); + } + + private final IImagingLoaderStrategy imageLoaderStrategy; + + private final RequestedImageSize imageSizeLimit; + + private final String singleChannelTransformationCodeOrNull; + + @Private + ImageLoadingHelper(IImagingLoaderStrategy imageLoaderStrategy, + RequestedImageSize imageSizeLimit, String singleChannelTransformationCodeOrNull) + { + this.imageLoaderStrategy = imageLoaderStrategy; + this.imageSizeLimit = imageSizeLimit; + this.singleChannelTransformationCodeOrNull = singleChannelTransformationCodeOrNull; + } + + ImageLoadingHelper(IImagingLoaderStrategy imageLoaderStrategy, Size imageSizeLimitOrNull, + String singleChannelTransformationCodeOrNull) + { + this(imageLoaderStrategy, new RequestedImageSize(imageSizeLimitOrNull, false), + singleChannelTransformationCodeOrNull); + } + + ImageLoadingHelper(DatasetAcquiredImagesReference imageChannels, IHierarchicalContentProvider contentProvider, + RequestedImageSize imageSizeLimit, String singleChannelTransformationCodeOrNull) + { + this(createLoaderStrategy(imageChannels, contentProvider), imageSizeLimit, singleChannelTransformationCodeOrNull); + } + + boolean isMergeAllChannels(DatasetAcquiredImagesReference imageChannels) + { + return imageChannels.isMergeAllChannels(getAllChannelCodes()); + } + + /** + * @param skipNonExisting if true references to non-existing images are ignored, otherwise an + * exception is thrown + * @param mergeAllChannels true if all existing channel images should be merged + * @param transformationInfo + */ + List<AbsoluteImageReference> fetchImageContents( + DatasetAcquiredImagesReference imagesReference, boolean mergeAllChannels, + boolean skipNonExisting, ImageTransformationParams transformationInfo) + { + List<String> channelCodes = imagesReference.getChannelCodes(getAllChannelCodes()); + List<AbsoluteImageReference> images = new ArrayList<AbsoluteImageReference>(); + for (String channelCode : channelCodes) + { + ImageChannelStackReference channelStackReference = imagesReference.getChannelStackReference(); + AbsoluteImageReference image = imageLoaderStrategy.tryGetImage(channelCode, channelStackReference, + imageSizeLimit, singleChannelTransformationCodeOrNull); + if (image == null && skipNonExisting == false) + { + String item = imageSizeLimit.isThumbnailRequired() ? "thumbnail" : "image"; + throw EnvironmentFailureException.fromTemplate("No %s found for channel stack %s and channel %s", + item, channelStackReference, channelCode); + } + if (image != null) + { + images.add(image); + } + } + + // Optimization for a case where all channels are on one image + if (mergeAllChannels && (false == shouldApplySingleChannelsTransformations(transformationInfo))) + { + AbsoluteImageReference allChannelsImageReference = tryCreateAllChannelsImageReference(images); + if (allChannelsImageReference != null) + { + images.clear(); + images.add(allChannelsImageReference); + } + } + return images; + } + + List<AbsoluteImageReference> getRepresentativeImageReferences(Location wellLocationOrNull) + { + List<AbsoluteImageReference> images = new ArrayList<AbsoluteImageReference>(); + + for (String chosenChannel : getAllChannelCodes()) + { + images.add(getRepresentativeImageReference(chosenChannel, wellLocationOrNull)); + } + return images; + } + + private List<String> getAllChannelCodes() + { + return imageLoaderStrategy.getImageParameters().getChannelsCodes(); + } + + /** + * @throw {@link EnvironmentFailureException} when image does not exist + */ + private AbsoluteImageReference getRepresentativeImageReference(String channelCode, + Location wellLocationOrNull) + { + AbsoluteImageReference image = imageLoaderStrategy.tryGetRepresentativeImage(channelCode, + wellLocationOrNull, imageSizeLimit, singleChannelTransformationCodeOrNull); + if (image != null) + { + return image; + } + String item = imageSizeLimit.isThumbnailRequired() ? "thumbnail" : "image"; + throw EnvironmentFailureException.fromTemplate("No representative %s found for well %s and channel %s", + item, wellLocationOrNull, channelCode); + } + + private boolean shouldApplySingleChannelsTransformations(ImageTransformationParams transformationInfo) + { + return transformationInfo != null + && transformationInfo.tryGetTransformationCodeForChannels() != null + && transformationInfo.tryGetTransformationCodeForChannels().size() > 0; + } + + // Checks if all images differ only at the color component level and stem from the same page + // of the same file. If that's the case any image from the collection contains the merged + // channels image (if we erase the color component). + private AbsoluteImageReference tryCreateAllChannelsImageReference( + List<AbsoluteImageReference> imageReferences) + { + AbsoluteImageReference lastFound = null; + for (AbsoluteImageReference image : imageReferences) + { + if (lastFound == null) + { + lastFound = image; + } else + { + if (equals(image.tryGetImageID(), lastFound.tryGetImageID()) == false + || image.getUniqueId().equals(lastFound.getUniqueId()) == false) + { + return null; + } + } + } + if (lastFound != null) + { + return lastFound.createWithoutColorComponent(); + } else + { + return null; + } + } + + private static boolean equals(String i1OrNull, String i2OrNull) + { + return (i1OrNull == null) ? (i2OrNull == null) : i1OrNull.equals(i2OrNull); + } +} \ No newline at end of file diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtilsTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtilsTest.java index eec32f3828b790984c618d7eae91ffd591bae173..e01e0b66f82525f3940f582c132f9079a0dcb405 100644 --- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtilsTest.java +++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtilsTest.java @@ -23,6 +23,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.commons.io.IOUtils; @@ -60,7 +61,7 @@ import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.InternalImageT /** * @author Franz-Josef Elmer */ -@Friend(toClasses = ImageChannelsUtils.class) +@Friend(toClasses = {ImageLoadingHelper.class, ImageChannelsUtils.class}) public class ImageChannelsUtilsTest extends AssertJUnit { public static final File TEST_IMAGE_FOLDER = new File("../screening/sourceTest/java/" @@ -83,6 +84,7 @@ public class ImageChannelsUtilsTest extends AssertJUnit for (int y = 0; y < height; y++) { int rgb = image.getRGB(x, y); + System.out.println(x+" "+y+" "+Integer.toHexString(rgb)); output.setRGB(x, y, (rgb & 0xff) << 16); } } @@ -122,8 +124,7 @@ public class ImageChannelsUtilsTest extends AssertJUnit try { - createImageChannelsUtils(null).calculateBufferedImage(imageRef, - createSingleChannelTransformationParams()); + createImage(imageRef, createSingleChannelTransformationParams(), null); fail("EnvironmentFailureException expected"); } catch (EnvironmentFailureException ex) { @@ -164,14 +165,25 @@ public class ImageChannelsUtilsTest extends AssertJUnit final DatasetAcquiredImagesReference imageRef = createDatasetAcquiredImagesReference(); prepareExpectations(absoluteImageReference, imageRef); + ImageTransformationParams transformationParams = createSingleChannelTransformationParams(); BufferedImage image = - createImageChannelsUtils(thumbnailSizeOrNull).calculateBufferedImage(imageRef, - createSingleChannelTransformationParams()); + createImage(imageRef, transformationParams, thumbnailSizeOrNull); assertEquals(expectedImageContentDescription, getImageContentDescription(image)); context.assertIsSatisfied(); } + private BufferedImage createImage(final DatasetAcquiredImagesReference imageRef, + ImageTransformationParams transformationParams, Size thumbnailSizeOrNull) + { + ImageLoadingHelper imageHelper = new ImageLoadingHelper(ImagingLoaderStrategyFactory.createImageLoaderStrategy(loader), + new RequestedImageSize(thumbnailSizeOrNull, false), null); + boolean mergeAllChannels = imageHelper.isMergeAllChannels(imageRef); + List<AbsoluteImageReference> imageContents = + imageHelper.fetchImageContents(imageRef, mergeAllChannels, false, transformationParams); + return ImageChannelsUtils.calculateBufferedImage(imageContents, transformationParams); + } + private void prepareExpectations(final AbsoluteImageReference absoluteImageReferenceOrNull, final DatasetAcquiredImagesReference imageRef) { @@ -214,7 +226,7 @@ public class ImageChannelsUtilsTest extends AssertJUnit String transformationCode = "MY_TRANSFORMATION"; Map<String, IImageTransformerFactory> transformations = createImageTransformationsMap(transformationCode, transformerFactory); - imgRef.getImageTransfomationFactories().setForChannel(transformations); + imgRef.getImageTransformationFactories().setForChannel(transformations); prepareExpectations(imgRef, imageRef); context.checking(new Expectations() @@ -225,13 +237,11 @@ public class ImageChannelsUtilsTest extends AssertJUnit } }); - BufferedImage image = - createImageChannelsUtils(null).calculateBufferedImage( - imageRef, - new ImageTransformationParams(true, false, transformationCode, - new HashMap<String, String>())); - assertEquals("e00 f00 f00 e00\n" + "f00 c00 c00 f00\n" + "f00 c00 c00 f00\n" - + "e00 f00 f00 e00\n", getImageContentDescription(image)); + ImageTransformationParams transformationParams = new ImageTransformationParams(true, false, transformationCode, + new HashMap<String, String>()); + BufferedImage image = createImage(imageRef, transformationParams, null); + assertEquals("0e0 0f0 0f0 0e0\n" + "0f0 0c0 0c0 0f0\n" + "0f0 0c0 0c0 0f0\n" + + "0e0 0f0 0f0 0e0\n", getImageContentDescription(image)); context.assertIsSatisfied(); } @@ -249,17 +259,10 @@ public class ImageChannelsUtilsTest extends AssertJUnit RequestedImageSize imageSize) { return new AbsoluteImageReference(image(fileName), "id42", null, null, imageSize, - new ChannelColorRGB(0, 0, 255), new ImageTransfomationFactories(), null, null, + new ChannelColorRGB(0, 255, 0), new ImageTransfomationFactories(), null, null, "ch2"); } - private ImageChannelsUtils createImageChannelsUtils(Size thumbnailSizeOrNull) - { - return new ImageChannelsUtils( - ImagingLoaderStrategyFactory.createImageLoaderStrategy(loader), - new RequestedImageSize(thumbnailSizeOrNull, false), null); - } - public void assertPNG(IHierarchicalContentNode image) { InputStream inputStream = image.getInputStream(); diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractScreeningSystemTestCase.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractScreeningSystemTestCase.java index a3cb97ab6bc3ac2e4c2ad12526c13282d613be02..b1931c71ecb682ecef159913552e358bbe817a15 100644 --- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractScreeningSystemTestCase.java +++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractScreeningSystemTestCase.java @@ -24,7 +24,10 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import javax.imageio.ImageIO; @@ -84,7 +87,7 @@ public abstract class AbstractScreeningSystemTestCase extends SystemTestCase public String toString() { StringBuilder builder = new StringBuilder(); - builder.append("/===== Failure Report for image ").append(referenceImage).append('\n'); + builder.append("/===== Failure Report for image ").append(referenceImage.getAbsolutePath()).append('\n'); builder.append(failureReport).append("\\============================\n"); return builder.toString(); } @@ -100,6 +103,7 @@ public abstract class AbstractScreeningSystemTestCase extends SystemTestCase { private final URLMethodWithParameters url; private final List<String> channels = new ArrayList<String>(); + private final Map<String, String> transformationsByChannel = new HashMap<String, String>(); private boolean mergeChannels = true; private boolean microscopy = false; private int wellRow = 1; @@ -146,6 +150,11 @@ public abstract class AbstractScreeningSystemTestCase extends SystemTestCase } url.addParameter(ImageServletUrlParameters.TILE_ROW_PARAM, Integer.toString(tileRow)); url.addParameter(ImageServletUrlParameters.TILE_COL_PARAM, Integer.toString(tileColumn)); + for (Entry<String, String> entry : transformationsByChannel.entrySet()) + { + url.addParameter(ImageServletUrlParameters.SINGLE_CHANNEL_TRANSFORMATION_CODE_PARAM + entry.getKey(), + entry.getValue()); + } url.addParameter("mode", mode); try { @@ -198,6 +207,13 @@ public abstract class AbstractScreeningSystemTestCase extends SystemTestCase channels.add(channel); return this; } + + public ImageLoader rescaling(String channel, int low, int high) + { + transformationsByChannel.put(channel, + ScreeningConstants.USER_DEFINED_RESCALING_CODE + "(" + low + "," + high + ")"); + return this; + } } /** @@ -208,6 +224,8 @@ public abstract class AbstractScreeningSystemTestCase extends SystemTestCase */ protected static final class ImageChecker { + private static final int HISTOGRAM_HEIGHT = 19; + private static final int HISTOGRAM_WIDTH = 151; private final StringBuilder failureReport = new StringBuilder(); private final File folderForWrongImages; @@ -230,8 +248,8 @@ public abstract class AbstractScreeningSystemTestCase extends SystemTestCase { if (assertNoFailuresAlreadyInvoked == false) { - AssertJUnit.assertEquals("", failureReport.toString()); assertNoFailuresAlreadyInvoked = true; + AssertJUnit.assertEquals("", failureReport.toString()); } } @@ -244,13 +262,12 @@ public abstract class AbstractScreeningSystemTestCase extends SystemTestCase FailureReport report = new FailureReport(referenceImage); try { - BufferedImage expectedImage = ImageIO.read(referenceImage); BufferedImage actualImage = imageLoader.load(); - checkEquals(report, expectedImage, actualImage); + checkEquals(report, referenceImage, actualImage); saveActualImageIfDifferent(actualImage, referenceImage, report); } catch (IOException ex) { - report.addFailureMessage("Couldn't load image: " + ex); + report.addFailureMessage("Couldn't save image: " + ex); } finally { if (report.isFailure()) @@ -260,6 +277,17 @@ public abstract class AbstractScreeningSystemTestCase extends SystemTestCase } } + private BufferedImage load(File referenceImage) + { + try + { + return ImageIO.read(referenceImage); + } catch (IOException ex) + { + return null; + } + } + private void saveActualImageIfDifferent(BufferedImage actualImage, File referenceImage, FailureReport report) throws IOException { if (report.isFailure() == false || actualImage == null) @@ -278,12 +306,17 @@ public abstract class AbstractScreeningSystemTestCase extends SystemTestCase } } - private void checkEquals(FailureReport report, BufferedImage expectedImage, BufferedImage actualImage) + private void checkEquals(FailureReport report, File referenceImage, BufferedImage actualImage) { - if (expectedImage == null || actualImage == null) + BufferedImage expectedImage = load(referenceImage); + if (expectedImage == null) + { + report.addFailureMessage("Expected and image is undefined"); + return; + } + if (actualImage == null) { - report.addFailureMessage("Expected and/or actual image is undefined: Expected image: <" + expectedImage - + ">, actual image: <" + actualImage + ">"); + report.addFailureMessage("Actual image is undefined"); return; } checkEquals(report, "Image height", expectedImage.getHeight(), actualImage.getHeight()); @@ -297,23 +330,32 @@ public abstract class AbstractScreeningSystemTestCase extends SystemTestCase checkEquals(report, "Color space type", expectedColorSpace.getType(), actualColorSpace.getType()); ImageHistogram expectedHistogram = ImageHistogram.calculateHistogram(expectedImage); ImageHistogram actualHistogram = ImageHistogram.calculateHistogram(actualImage); - String expectedRenderedHistogram = expectedHistogram.renderAsASCIIChart(49, 9); - String actualRenderedHistogram = actualHistogram.renderAsASCIIChart(49, 9); + String expectedRenderedHistogram = expectedHistogram.renderAsASCIIChart(HISTOGRAM_WIDTH, HISTOGRAM_HEIGHT); + String actualRenderedHistogram = actualHistogram.renderAsASCIIChart(HISTOGRAM_WIDTH, HISTOGRAM_HEIGHT); if (expectedRenderedHistogram.equals(actualRenderedHistogram)) { + printComparisonMessage(referenceImage, expectedRenderedHistogram); return; } // difference can be caused by a value close to discretization border. Thus try another height. - expectedRenderedHistogram = expectedHistogram.renderAsASCIIChart(49, 10); - actualRenderedHistogram = actualHistogram.renderAsASCIIChart(49, 10); + expectedRenderedHistogram = expectedHistogram.renderAsASCIIChart(HISTOGRAM_WIDTH, HISTOGRAM_HEIGHT + 1); + actualRenderedHistogram = actualHistogram.renderAsASCIIChart(HISTOGRAM_WIDTH, HISTOGRAM_HEIGHT + 1); if (expectedRenderedHistogram.equals(actualRenderedHistogram)) { + printComparisonMessage(referenceImage, expectedRenderedHistogram); return; } - report.addFailureMessage("expected histogram of " + expectedImage + ":\n" + expectedRenderedHistogram + report.addFailureMessage("expected histogram of " + expectedImage + ":\n" + expectedRenderedHistogram + "actual histogram of " + actualImage + ":\n" + actualRenderedHistogram); } - + + private void printComparisonMessage(File referenceImage, + String expectedRenderedHistogram) + { + System.out.println("Histogram of reference image (" + referenceImage + + ") equals actual histogram:\n" + expectedRenderedHistogram); + } + private void checkEquals(FailureReport report, String message, Object expected, Object actual) { if (expected == null ? expected == actual : expected.equals(actual)) 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 7530bb282620a0ce2d99915b4ba2a1cc5f99bdf0..132e6918a6b1acdb14c489a95c84d7479909d028 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 @@ -107,6 +107,10 @@ 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.assertNoFailures(); } diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java index 7e82c70dfe40254e6535ecba11b3cbc5ad40bc44..adf74e7430184618e780133f1bcc8521c9e7d992 100644 --- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java +++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java @@ -54,6 +54,10 @@ public class SimpleImageDropboxTest extends AbstractImageDropboxTestCase new ImageLoader(dataSet, sessionToken).channel("DAPI")); imageChecker.check(new File(getTestDataFolder(), "1_3_DAPI_CY3_256x191.png"), new ImageLoader(dataSet, sessionToken).tileColumn(3).channel("DAPI").channel("CY3").mode("thumbnail256x191")); + imageChecker.check(new File(getTestDataFolder(), "1_2_Merged_1392x1040.png"), + new ImageLoader(dataSet, sessionToken).tileColumn(2).mode("thumbnail1392x1040")); + imageChecker.check(new File(getTestDataFolder(), "1_1_Merged_Default_GFP_0_100.png"), + new ImageLoader(dataSet, sessionToken).rescaling("GFP", 0, 100)); imageChecker.assertNoFailures(); } }