From 62be1a06215075aca040bf6c84a54410682ffc8f Mon Sep 17 00:00:00 2001
From: gpawel <gpawel>
Date: Tue, 18 Dec 2012 16:39:04 +0000
Subject: [PATCH] SP-431 SOB-6: Allow custom image scaling of merged images

SVN: 27957
---
 .../dss/etl/AbsoluteImageReference.java       |  16 ++-
 .../etl/dataaccess/ImagingDatasetLoader.java  |   2 +-
 .../ImageGenerationDescriptionFactory.java    |  41 +++++-
 .../server/images/ImageChannelsUtils.java     |  71 +++++++---
 .../dto/ImageGenerationDescription.java       |  10 ++
 .../images/dto/ImageTransformationParams.java |  26 +++-
 .../detailviewers/ChannelChooser.java         |  29 ++--
 .../detailviewers/ChannelChooserPanel.java    | 126 ++++++++++++++----
 .../ChannelWidgetWithListener.java            |   7 +-
 .../MaterialReplicaSummaryComponent.java      |   8 +-
 .../UserDefinedRescalingSettingsDialog.java   | 125 ++++++++++++-----
 .../detailviewers/WellContentDialog.java      |   5 +-
 .../detailviewers/WellSearchGrid.java         |   7 +-
 .../dto/LogicalImageChannelsReference.java    |  15 ++-
 .../detailviewers/image/Image.java            |  43 ++++--
 .../client/web/public/screening-dictionary.js |   4 +-
 .../server/logic/HCSImageDatasetLoader.java   |   1 +
 .../ImageSizeFeedingMaintenanceTaskTest.java  |   4 +-
 .../server/images/ImageChannelsUtilsTest.java |  15 ++-
 .../server/DssServiceRpcScreeningTest.java    |   8 +-
 20 files changed, 427 insertions(+), 136 deletions(-)

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 9283687317c..258036cb404 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
@@ -50,6 +50,8 @@ public class AbsoluteImageReference extends AbstractImageReference
 
     private String singleChannelTransformationCodeOrNull;
 
+    private final String channelCodeOrNull;
+
     /**
      * @param contentNode is the original content before choosing the color component and the image
      *            ID
@@ -58,7 +60,8 @@ public class AbsoluteImageReference extends AbstractImageReference
             String imageIdOrNull, ColorComponent colorComponentOrNull,
             RequestedImageSize imageSize, ChannelColorRGB channelColor,
             ImageTransfomationFactories imageTransfomationFactories,
-            ImageLibraryInfo imageLibraryOrNull, String singleChannelTransformationCodeOrNull)
+            ImageLibraryInfo imageLibraryOrNull, String singleChannelTransformationCodeOrNull,
+            String channelCodeOrNull)
     {
         super(imageIdOrNull, colorComponentOrNull);
         assert imageSize != null : "image size is null";
@@ -71,6 +74,7 @@ public class AbsoluteImageReference extends AbstractImageReference
         this.imageTransfomationFactories = imageTransfomationFactories;
         this.imageLibraryOrNull = imageLibraryOrNull;
         this.singleChannelTransformationCodeOrNull = singleChannelTransformationCodeOrNull;
+        this.channelCodeOrNull = channelCodeOrNull;
     }
 
     /**
@@ -136,16 +140,24 @@ public class AbsoluteImageReference extends AbstractImageReference
         return channelColor;
     }
 
+    /**
+     * Returns the applied transformation code
+     */
     public String tryGetSingleChannelTransformationCode()
     {
         return singleChannelTransformationCodeOrNull;
     }
 
+    public String tryGetChannelCode()
+    {
+        return channelCodeOrNull;
+    }
+
     public AbsoluteImageReference createWithoutColorComponent()
     {
         ColorComponent colorComponent = null;
         return new AbsoluteImageReference(contentNode, uniqueId, tryGetImageID(), colorComponent,
                 imageSize, channelColor, imageTransfomationFactories, imageLibraryOrNull,
-                singleChannelTransformationCodeOrNull);
+                singleChannelTransformationCodeOrNull, null);
     }
 }
\ No newline at end of file
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingDatasetLoader.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingDatasetLoader.java
index 794d196d34d..0b32b0a2501 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingDatasetLoader.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingDatasetLoader.java
@@ -177,7 +177,7 @@ public class ImagingDatasetLoader extends HCSDatasetLoader implements IImagingDa
         ImageLibraryInfo imageLibrary = tryGetImageLibrary(dataset, useNativeImageLibrary);
         return new AbsoluteImageReference(contentNode, path, image.getImageID(), colorComponent,
                 imageSize, getColor(channel), imageTransfomationFactories, imageLibrary,
-                image.tryGetSingleChannelTransformationCode());
+                image.tryGetSingleChannelTransformationCode(), channel.getCode());
     }
 
     private static ChannelColorRGB getColor(ImgChannelDTO channel)
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ImageGenerationDescriptionFactory.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ImageGenerationDescriptionFactory.java
index 26da2da889b..a750ad7f60e 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ImageGenerationDescriptionFactory.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ImageGenerationDescriptionFactory.java
@@ -27,11 +27,13 @@ import java.util.Map.Entry;
 import javax.servlet.http.HttpServletRequest;
 
 import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.common.shared.basic.string.StringUtils;
 import ch.systemsx.cisd.hcs.Location;
 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.ImageGenerationDescription;
 import ch.systemsx.cisd.openbis.dss.generic.shared.dto.Size;
+import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ScreeningConstants;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ScreeningConstants.ImageServletUrlParameters;
 
 /**
@@ -83,9 +85,44 @@ class ImageGenerationDescriptionFactory
 
         String singleChannelTransformationCodeOrNull =
                 request.getParameter(ImageServletUrlParameters.SINGLE_CHANNEL_TRANSFORMATION_CODE_PARAM);
+
+        Map<String, String> transformationsPerChannel = extractTransformationsPerChannel(request);
+
         return new ImageGenerationDescription(channelsToMerge,
-                singleChannelTransformationCodeOrNull, overlayChannels, sessionId,
-                thumbnailSizeOrNull);
+                singleChannelTransformationCodeOrNull, transformationsPerChannel, overlayChannels,
+                sessionId, thumbnailSizeOrNull);
+    }
+
+    private static Map<String, String> extractTransformationsPerChannel(HttpServletRequest request)
+    {
+        Map<String, String> transformationsPerChannel = new HashMap<String, String>();
+
+        Enumeration<?> params = request.getParameterNames();
+        while (params.hasMoreElements())
+        {
+            String paramName = (String) params.nextElement();
+            if (paramName
+                    .startsWith(ScreeningConstants.ImageServletUrlParameters.SINGLE_CHANNEL_TRANSFORMATION_CODE_PARAM))
+            {
+                if (paramName
+                        .equals(ScreeningConstants.ImageServletUrlParameters.SINGLE_CHANNEL_TRANSFORMATION_CODE_PARAM))
+                {
+                    continue;
+                }
+
+                String channelCode =
+                        paramName
+                                .substring(ScreeningConstants.ImageServletUrlParameters.SINGLE_CHANNEL_TRANSFORMATION_CODE_PARAM
+                                        .length());
+                String transformationCode = request.getParameter(paramName);
+                if (StringUtils.isNotBlank(transformationCode))
+                {
+                    transformationsPerChannel.put(channelCode, transformationCode);
+                }
+            }
+        }
+
+        return transformationsPerChannel;
     }
 
     private static List<DatasetAcquiredImagesReference> getOverlayChannels(
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 e1b839ff3e4..8f5fb1cac20 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
@@ -23,7 +23,9 @@ 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;
+import java.util.Map;
 
 import org.apache.log4j.Logger;
 
@@ -119,8 +121,8 @@ public class ImageChannelsUtils
             RequestedImageSize imageSize = new RequestedImageSize(thumbnailSizeOrNull, false);
             image =
                     calculateBufferedImage(imageChannels,
-                            params.tryGetSingleChannelTransformationCode(), contentProvider,
-                            imageSize);
+                            params.tryGetSingleChannelTransformationCode(),
+                            params.tryGetTransformationsPerChannel(), contentProvider, imageSize);
         }
 
         RequestedImageSize overlaySize = calcOverlaySize(image, thumbnailSizeOrNull);
@@ -158,11 +160,13 @@ public class ImageChannelsUtils
                 createImageChannelsUtils(imagesReference, contentProvider, imageSize,
                         singleChannelTransformationCodeOrNull);
         boolean mergeAllChannels = utils.isMergeAllChannels(imagesReference);
-        List<AbsoluteImageReference> imageContents =
-                utils.fetchImageContents(imagesReference, mergeAllChannels, true);
         ImageTransformationParams transformationInfo =
-                new ImageTransformationParams(true, mergeAllChannels, null);
-        return calculateSingleImagesForDisplay(imageContents, transformationInfo, 0.0f);
+                new ImageTransformationParams(true, mergeAllChannels, null,
+                        new HashMap<String, String>());
+        List<AbsoluteImageReference> imageContents =
+                utils.fetchImageContents(imagesReference, mergeAllChannels, true,
+                        transformationInfo);
+        return calculateSingleImagesForDisplay(imageContents, transformationInfo, 0.0f, null);
     }
 
     private static RequestedImageSize getSize(BufferedImage img, boolean highQuality)
@@ -203,6 +207,7 @@ public class ImageChannelsUtils
     private static BufferedImage calculateBufferedImage(
             DatasetAcquiredImagesReference imageChannels,
             String singleChannelTransformationCodeOrNull,
+            Map<String, String> transformationsPerChannels,
             IHierarchicalContentProvider contentProvider, RequestedImageSize imageSizeLimit)
     {
         ImageChannelsUtils imageChannelsUtils =
@@ -212,7 +217,7 @@ public class ImageChannelsUtils
                 imageChannelsUtils.isMergeAllChannels(imageChannels);
         ImageTransformationParams transformationInfo =
                 new ImageTransformationParams(true, useMergedChannelsTransformation,
-                        singleChannelTransformationCodeOrNull);
+                        singleChannelTransformationCodeOrNull, transformationsPerChannels);
 
         return imageChannelsUtils.calculateBufferedImage(imageChannels, transformationInfo);
     }
@@ -234,7 +239,7 @@ public class ImageChannelsUtils
     {
         boolean mergeAllChannels = isMergeAllChannels(imageChannels);
         List<AbsoluteImageReference> imageContents =
-                fetchImageContents(imageChannels, mergeAllChannels, false);
+                fetchImageContents(imageChannels, mergeAllChannels, false, transformationInfo);
         return calculateBufferedImage(imageContents, transformationInfo);
     }
 
@@ -247,10 +252,11 @@ public class ImageChannelsUtils
      * @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)
+            boolean skipNonExisting, ImageTransformationParams transformationInfo)
     {
         List<String> channelCodes = imagesReference.getChannelCodes(getAllChannelCodes());
         List<AbsoluteImageReference> images = new ArrayList<AbsoluteImageReference>();
@@ -272,7 +278,8 @@ public class ImageChannelsUtils
         }
 
         // Optimization for a case where all channels are on one image
-        if (mergeAllChannels)
+        if (mergeAllChannels
+                && (false == shouldApplySingleChannelsTransformations(transformationInfo)))
         {
             AbsoluteImageReference allChannelsImageReference =
                     tryCreateAllChannelsImageReference(images);
@@ -285,6 +292,19 @@ public class ImageChannelsUtils
         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)
@@ -309,7 +329,7 @@ public class ImageChannelsUtils
                         .getRepresentativeImageReferences(wellLocationOrNull);
         BufferedImage image =
                 calculateBufferedImage(imageReferences, new ImageTransformationParams(true, true,
-                        null));
+                        null, new HashMap<String, String>()));
         String name = createFileName(datasetCode, wellLocationOrNull, imageSizeLimitOrNull);
         return createResponseContentStream(image, name);
     }
@@ -369,17 +389,19 @@ public class ImageChannelsUtils
                 new ImageChannelsUtils(imageLoaderStrategy, imageSizeLimitOrNull,
                         singleChannelImageTransformationCodeOrNull);
         boolean mergeAllChannels = imageChannelsUtils.isMergeAllChannels(imagesReference);
+        ImageTransformationParams transformationInfo =
+                new ImageTransformationParams(transform, mergeAllChannels,
+                        singleChannelImageTransformationCodeOrNull, new HashMap<String, String>());
+
         List<AbsoluteImageReference> imageContents =
-                imageChannelsUtils.fetchImageContents(imagesReference, mergeAllChannels, false);
+                imageChannelsUtils.fetchImageContents(imagesReference, mergeAllChannels, false,
+                        transformationInfo);
 
         IHierarchicalContentNode contentNode = tryGetRawContent(convertToPng, imageContents);
         if (contentNode != null)
         {
             return contentNode;
         }
-        ImageTransformationParams transformationInfo =
-                new ImageTransformationParams(transform, mergeAllChannels,
-                        singleChannelImageTransformationCodeOrNull);
         BufferedImage image = calculateBufferedImage(imageContents, transformationInfo);
         return createPngContent(image, null);
     }
@@ -541,7 +563,7 @@ public class ImageChannelsUtils
     {
         // We do not transform single images here.
         List<ImageWithReference> images =
-                calculateSingleImagesForDisplay(imageReferences, null, null);
+                calculateSingleImagesForDisplay(imageReferences, null, null, transformationInfo);
         BufferedImage mergedImage = mergeImages(images);
         // NOTE: even if we are not merging all the channels but just few of them we use the
         // merged-channel transformation
@@ -626,10 +648,12 @@ public class ImageChannelsUtils
     /**
      * @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 transformationInfoOrNull, Float threshold,
+            ImageTransformationParams transformationInfoForMergingOrNull)
     {
         List<ImageWithReference> images = new ArrayList<ImageWithReference>();
         for (AbsoluteImageReference imageRef : imageReferences)
@@ -640,6 +664,19 @@ public class ImageChannelsUtils
                 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
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/dto/ImageGenerationDescription.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/dto/ImageGenerationDescription.java
index 88579d5892f..3cf10a5b86e 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/dto/ImageGenerationDescription.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/dto/ImageGenerationDescription.java
@@ -17,6 +17,7 @@
 package ch.systemsx.cisd.openbis.dss.generic.server.images.dto;
 
 import java.util.List;
+import java.util.Map;
 
 import ch.systemsx.cisd.hcs.Location;
 import ch.systemsx.cisd.openbis.dss.generic.server.images.dto.ImageChannelStackReference.HCSChannelStackByLocationReference;
@@ -34,6 +35,8 @@ public class ImageGenerationDescription
 
     private final String singleChannelTransformationCodeOrNull;
 
+    private final Map<String, String> transformationsPerChannelOrNull;
+
     private final List<DatasetAcquiredImagesReference> overlayChannels;
 
     private final String sessionId;
@@ -43,11 +46,13 @@ public class ImageGenerationDescription
 
     public ImageGenerationDescription(DatasetAcquiredImagesReference imageChannelsOrNull,
             String singleChannelTransformationCodeOrNull,
+            Map<String, String> transformationsPerChannelOrNull,
             List<DatasetAcquiredImagesReference> overlayChannels, String sessionId,
             Size thumbnailSizeOrNull)
     {
         this.imageChannelsOrNull = imageChannelsOrNull;
         this.singleChannelTransformationCodeOrNull = singleChannelTransformationCodeOrNull;
+        this.transformationsPerChannelOrNull = transformationsPerChannelOrNull;
         this.overlayChannels = overlayChannels;
         this.sessionId = sessionId;
         this.thumbnailSizeOrNull = thumbnailSizeOrNull;
@@ -63,6 +68,11 @@ public class ImageGenerationDescription
         return singleChannelTransformationCodeOrNull;
     }
 
+    public Map<String, String> tryGetTransformationsPerChannel()
+    {
+        return transformationsPerChannelOrNull;
+    }
+
     public List<DatasetAcquiredImagesReference> getOverlayChannels()
     {
         return overlayChannels;
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/dto/ImageTransformationParams.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/dto/ImageTransformationParams.java
index 4450df0b274..bfbcbfdb20c 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/dto/ImageTransformationParams.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/dto/ImageTransformationParams.java
@@ -16,6 +16,8 @@
 
 package ch.systemsx.cisd.openbis.dss.generic.server.images.dto;
 
+import java.util.Map;
+
 /**
  * Describes which image transformations should be applied. Note that image-level transformation is
  * always applied for single channels.
@@ -35,12 +37,16 @@ public class ImageTransformationParams
      */
     private final String singleChannelTransformationCodeOrNull;
 
+    private Map<String, String> transformationsPerChannel;
+
     public ImageTransformationParams(boolean applyNonImageLevelTransformation,
-            boolean useMergedChannelsTransformation, String singleChannelTransformationCodeOrNull)
+            boolean useMergedChannelsTransformation, String singleChannelTransformationCodeOrNull,
+            Map<String, String> transformationsPerChannel)
     {
         this.applyNonImageLevelTransformation = applyNonImageLevelTransformation;
         this.useMergedChannelsTransformation = useMergedChannelsTransformation;
         this.singleChannelTransformationCodeOrNull = singleChannelTransformationCodeOrNull;
+        this.transformationsPerChannel = transformationsPerChannel;
     }
 
     public boolean isApplyNonImageLevelTransformation()
@@ -57,4 +63,22 @@ public class ImageTransformationParams
     {
         return singleChannelTransformationCodeOrNull;
     }
+
+    public String tryGetTransformationCodeForChannel(String channelCode)
+    {
+        if (channelCode == null || transformationsPerChannel == null)
+        {
+            return null;
+        }
+        return transformationsPerChannel.get(channelCode);
+    }
+
+    public Map<String, String> tryGetTransformationCodeForChannels()
+    {
+        if (transformationsPerChannel == null)
+        {
+            return null;
+        }
+        return transformationsPerChannel;
+    }
 }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelChooser.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelChooser.java
index 849d86e9916..9b42e61d8b8 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelChooser.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelChooser.java
@@ -17,6 +17,7 @@
 package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -53,6 +54,7 @@ import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ImageResolutio
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.IntensityRange;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.InternalImageChannel;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.InternalImageTransformationInfo;
+import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ScreeningConstants;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.WellSearchCriteria.AnalysisProcedureCriteria;
 
 /**
@@ -95,7 +97,7 @@ class ChannelChooser
 
     private String imageTransformationCodeOrNull;
 
-    private IntensityRange rangeOrNull;
+    private Map<String, IntensityRange> rangesOrNull;
 
     public ChannelChooser(LogicalImageReference basicImage, IChanneledViewerFactory viewerFactory,
             IDefaultChannelState defaultChannelState)
@@ -109,7 +111,7 @@ class ChannelChooser
         this.imageTransformationCodeOrNull =
                 tryGetInitialImageTransformationCode(defaultChannelState, basicChannelCodes,
                         basicImage.getImagetParameters());
-        this.rangeOrNull = tryGetInitialIntensityRange(defaultChannelState, basicChannelCodes);
+        this.rangesOrNull = tryGetInitialIntensityRange(defaultChannelState, basicChannelCodes);
         this.defaultChannelState = defaultChannelState;
         this.selectedOverlayChannels = new HashSet<ImageDatasetChannel>();
     }
@@ -126,7 +128,7 @@ class ChannelChooser
     {
         LogicalImageChannelsReference state =
                 new LogicalImageChannelsReference(basicImage, basicChannelCodes,
-                        imageTransformationCodeOrNull, rangeOrNull, selectedOverlayChannels);
+                        imageTransformationCodeOrNull, rangesOrNull, selectedOverlayChannels);
 
         LayoutContainer view =
                 viewerFactory.create(state,
@@ -337,12 +339,12 @@ class ChannelChooser
                         public void selectionChanged(List<String> newlySelectedChannels,
                                 @SuppressWarnings("hiding")
                                 String imageTransformationCodeOrNull, @SuppressWarnings("hiding")
-                                IntensityRange rangeOrNull)
+                                Map<String, IntensityRange> rangesOrNull)
                         {
                             basicChannelCodes = newlySelectedChannels;
                             ChannelChooser.this.imageTransformationCodeOrNull =
                                     imageTransformationCodeOrNull;
-                            ChannelChooser.this.rangeOrNull = rangeOrNull;
+                            ChannelChooser.this.rangesOrNull = rangesOrNull;
                             refresh();
                         }
                     });
@@ -387,9 +389,10 @@ class ChannelChooser
             IDefaultChannelState defaultChannelState, List<String> channels,
             ImageDatasetParameters imageParameters)
     {
-        if (imageParameters != null && channels.size() == 1)
+        if (imageParameters != null)
         {
-            String channel = channels.get(0);
+            String channel =
+                    channels.size() == 1 ? channels.get(0) : ScreeningConstants.MERGED_CHANNELS;
             String initialTransformation = defaultChannelState.tryGetDefaultTransformation(channel);
             if (ChannelChooserPanel.DEFAULT_TRANSFORMATION_CODE.equals(initialTransformation))
             {
@@ -417,17 +420,17 @@ class ChannelChooser
         return null;
     }
 
-    private static IntensityRange tryGetInitialIntensityRange(
+    private static Map<String, IntensityRange> tryGetInitialIntensityRange(
             IDefaultChannelState defaultChannelState, List<String> channels)
     {
-        if (channels.size() == 1)
+        Map<String, IntensityRange> ranges = new HashMap<String, IntensityRange>();
+
+        for (String channel : channels)
         {
-            String channel = channels.get(0);
             IntensityRange rangeOrNull = defaultChannelState.tryGetIntensityRange(channel);
-
-            return rangeOrNull;
+            ranges.put(channel, rangeOrNull);
         }
 
-        return null;
+        return ranges;
     }
 }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelChooserPanel.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelChooserPanel.java
index 0012c3edaeb..80f78a9deeb 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelChooserPanel.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelChooserPanel.java
@@ -74,7 +74,7 @@ public class ChannelChooserPanel extends LayoutContainer
     public static interface ChannelSelectionListener
     {
         void selectionChanged(List<String> channels, String imageTransformationCodeOrNull,
-                IntensityRange rangeOrNull);
+                Map<String, IntensityRange> rangesOrNull);
     }
 
     public static final String DEFAULT_TRANSFORMATION_CODE = "$DEFAULT$";
@@ -126,29 +126,37 @@ public class ChannelChooserPanel extends LayoutContainer
             @Override
             public void handleEvent(BaseEvent be)
             {
-                String channelCode = getSelectedValues().get(0);
+                List<LabeledItem<String>> channelCodes = getSelectedLabeledValues();
                 InternalImageTransformationInfo selectedTransformation =
                         transformationsComboBox.getSimpleValue().getItem();
                 String transformationCode = selectedTransformation.getCode();
-                defaultChannelState.setDefaultTransformation(channelCode, transformationCode);
+                defaultChannelState.setDefaultTransformation(getChannelCode(channelCodes),
+                        transformationCode);
                 changeTransformationSettingsButtonVisibility(true, false);
 
-                IntensityRange intensityRange =
-                        defaultChannelState.tryGetIntensityRange(channelCode);
+                Map<String, IntensityRange> intensityRanges = new HashMap<String, IntensityRange>();
+                for (LabeledItem<String> channelCode : channelCodes)
+                {
+                    intensityRanges.put(channelCode.getItem(),
+                            defaultChannelState.tryGetIntensityRange(channelCode.getItem()));
+                }
                 notifySelectionListeners(getSelectedValues(),
-                        tryGetSelectedTransformationCode(false), intensityRange);
+                        tryGetSelectedTransformationCode(false), intensityRanges);
 
                 String updatedTooltip = selectedTransformation.getLabel();
                 if (ScreeningConstants.USER_DEFINED_RESCALING_CODE
                         .equalsIgnoreCase(transformationCode))
                 {
-                    updatedTooltip +=
-                            " ["
-                                    + (intensityRange == null ? "undefined" : intensityRange
-                                            .getBlackPoint()
-                                            + " - "
-                                            + intensityRange.getWhitePoint()) + "]";
-                    ChannelChooserPanel.this.intensitiesPerChannel.put(channelCode, intensityRange);
+                    /*
+                     * TODO updatedTooltip += " [" + (intensityRange == null ? "undefined" :
+                     * intensityRange .getBlackPoint() + " - " + intensityRange.getWhitePoint()) +
+                     * "]";
+                     */
+                    for (Map.Entry<String, IntensityRange> rangeEntry : intensityRanges.entrySet())
+                    {
+                        ChannelChooserPanel.this.intensitiesPerChannel.put(rangeEntry.getKey(),
+                                rangeEntry.getValue());
+                    }
                 }
                 transformationsComboBox.setToolTip(updatedTooltip);
             }
@@ -192,7 +200,7 @@ public class ChannelChooserPanel extends LayoutContainer
                             UserDefinedRescalingSettingsDialog dialog =
                                     new UserDefinedRescalingSettingsDialog(messageProvider,
                                             intensitiesPerChannel, defaultChannelState,
-                                            getSelectedValues().get(0));
+                                            getSelectedLabeledValues());
                             dialog.addListener(Events.OnChange, transformationSelection);
                             dialog.show();
                         }
@@ -207,6 +215,16 @@ public class ChannelChooserPanel extends LayoutContainer
         updateChannelSelection(selectedChannelsOrNull);
     }
 
+    private final static String getChannelCode(List<LabeledItem<String>> selection)
+    {
+        if (selection.size() == 1)
+        {
+            return selection.get(0).getItem();
+        }
+
+        return ScreeningConstants.MERGED_CHANNELS;
+    }
+
     private SimpleModelComboBox<String> createChannelsComboBox()
     {
         SimpleModelComboBox<String> comboBox =
@@ -287,6 +305,50 @@ public class ChannelChooserPanel extends LayoutContainer
         return channels;
     }
 
+    /**
+     * @return the current channel selection. If all channels are selected, returns whole list of
+     *         channels.
+     */
+    public List<LabeledItem<String>> getSelectedLabeledValues()
+    {
+        LabeledItem<String> comboBoxValue;
+        if (channelsComboBox.getSelectedIndex() == -1)
+        {
+            comboBoxValue = null;
+        } else
+        {
+            comboBoxValue = channelsComboBox.getSimpleValue();
+        }
+
+        if (comboBoxValue == null)
+        {
+            return Collections.<LabeledItem<String>> emptyList();
+        }
+        if (ScreeningConstants.MERGED_CHANNELS.equals(comboBoxValue.getItem()))
+        {
+            // multiple channel selection
+            return getMergedChannelLabeledSelection();
+        } else
+        {
+            // single channel selection
+            return Collections.singletonList(comboBoxValue);
+        }
+    }
+
+    private List<LabeledItem<String>> getMergedChannelLabeledSelection()
+    {
+        List<LabeledItem<String>> channels = new ArrayList<LabeledItem<String>>();
+        for (CheckBox cb : getAllCheckBoxes())
+        {
+            if (cb.getValue() == true)
+            {
+                channels.add(new LabeledItem<String>(cb.getValueAttribute(), cb.getBoxLabel()));
+            }
+        }
+
+        return channels;
+    }
+
     private void addChannelsForParameters(ImageDatasetParameters imageParameters)
     {
         addChannelToComboBox(new LabeledItem<String>(MERGED_CHANNELS, MERGED_CHANNELS));
@@ -312,6 +374,8 @@ public class ChannelChooserPanel extends LayoutContainer
             transformationsForChannel.addAll(imageParameters
                     .getAvailableImageTransformationsFor(code));
 
+            intensitiesPerChannel.put(code, defaultChannelState.tryGetIntensityRange(code));
+
             if (codeAdded)
             {
                 // also add a checkBockbox for the channel
@@ -408,7 +472,7 @@ public class ChannelChooserPanel extends LayoutContainer
         updateTransformationComboBox();
 
         notifySelectionListeners(selection, tryGetSelectedTransformationCode(false),
-                tryGetSelectedIntensityRange());
+                tryGetSelectedIntensityRanges());
     }
 
     public String tryGetSelectedTransformationCode(boolean force)
@@ -428,16 +492,21 @@ public class ChannelChooserPanel extends LayoutContainer
         return transformCode(code);
     }
 
-    public IntensityRange tryGetSelectedIntensityRange()
+    public Map<String, IntensityRange> tryGetSelectedIntensityRanges()
     {
-        List<String> selectedValues = getSelectedValues();
-        if (selectedValues != null && selectedValues.size() > 0)
+        List<LabeledItem<String>> selectedValues = getSelectedLabeledValues();
+        if (selectedValues != null)
         {
-            return intensitiesPerChannel.get(getSelectedValues().get(0));
-        } else
-        {
-            return null;
+            Map<String, IntensityRange> ranges = new HashMap<String, IntensityRange>();
+            for (LabeledItem<String> channelCode : selectedValues)
+            {
+                ranges.put(channelCode.getItem(), intensitiesPerChannel.get(channelCode.getItem()));
+            }
+
+            return ranges;
         }
+
+        return null;
     }
 
     private static String transformCode(String code)
@@ -452,11 +521,11 @@ public class ChannelChooserPanel extends LayoutContainer
     }
 
     private void notifySelectionListeners(List<String> selection,
-            String imageTransformationCodeOrNull, IntensityRange rangeOrNull)
+            String imageTransformationCodeOrNull, Map<String, IntensityRange> rangesOrNull)
     {
         for (ChannelSelectionListener listener : channelSelectionListeners)
         {
-            listener.selectionChanged(selection, imageTransformationCodeOrNull, rangeOrNull);
+            listener.selectionChanged(selection, imageTransformationCodeOrNull, rangesOrNull);
         }
     }
 
@@ -569,6 +638,15 @@ public class ChannelChooserPanel extends LayoutContainer
 
             selectTransformation(selectedValues.get(0));
             setTransformationsVisible(true);
+        } else if (selectedValues.size() > 0)
+        {
+            model.add(DEFAULT_TRANSFORMATION);
+            model.add(convertToLabeledItem(new InternalImageTransformationInfo(
+                    ScreeningConstants.USER_DEFINED_RESCALING_CODE, "User defined",
+                    "User defined intensity rescaling", "", false)));
+            transformationsComboBox.add(model);
+            selectTransformation(ScreeningConstants.MERGED_CHANNELS);
+            setTransformationsVisible(true);
         } else
         {
             setTransformationsVisible(false);
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelWidgetWithListener.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelWidgetWithListener.java
index 0e83909df37..c7e5615bc47 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelWidgetWithListener.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/ChannelWidgetWithListener.java
@@ -17,6 +17,7 @@
 package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers;
 
 import java.util.List;
+import java.util.Map;
 
 import com.extjs.gxt.ui.client.widget.LayoutContainer;
 import com.google.gwt.user.client.ui.Widget;
@@ -40,7 +41,7 @@ public class ChannelWidgetWithListener implements ChannelChooserPanel.ChannelSel
     interface ISimpleChanneledViewerFactory
     {
         Widget create(List<String> channelCodes, String imageTransformationCodeOrNull,
-                IntensityRange rangeOrNull);
+                Map<String, IntensityRange> rangesOrNull);
     }
 
     public ChannelWidgetWithListener(final ISimpleChanneledViewerFactory viewerFactory)
@@ -56,12 +57,12 @@ public class ChannelWidgetWithListener implements ChannelChooserPanel.ChannelSel
 
     @Override
     public void selectionChanged(List<String> channelNames, String imageTransformationCodeOrNull,
-            IntensityRange rangeOrNull)
+            Map<String, IntensityRange> rangesOrNull)
     {
         if (channelNames != null)
         {
             GuiUtils.replaceLastItem(container,
-                    viewerFactory.create(channelNames, imageTransformationCodeOrNull, rangeOrNull));
+                    viewerFactory.create(channelNames, imageTransformationCodeOrNull, rangesOrNull));
         }
     }
 }
\ No newline at end of file
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/MaterialReplicaSummaryComponent.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/MaterialReplicaSummaryComponent.java
index fc2bcda9326..7b1538095b4 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/MaterialReplicaSummaryComponent.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/MaterialReplicaSummaryComponent.java
@@ -59,11 +59,11 @@ import ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.d
 import ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers.utils.PropertiesUtil;
 import ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.ui.columns.specific.ScreeningLinkExtractor;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ImageDatasetParameters;
+import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.IntensityRange;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.WellContent;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.WellReplicaImage;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.WellSearchCriteria.AnalysisProcedureCriteria;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.WellSearchCriteria.ExperimentSearchCriteria;
-import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.IntensityRange;
 
 /**
  * Component which for a specified material and experiment presents 1. feature vectors (detailed and
@@ -347,17 +347,17 @@ public class MaterialReplicaSummaryComponent
             {
                 @Override
                 public Widget create(List<String> channels, String imageTransformationCodeOrNull,
-                        IntensityRange rangeOrNull)
+                        Map<String, IntensityRange> rangesOrNull)
                 {
                     return WellContentDialog.createImageViewerForChannel(screeningViewContext,
                             image, oneImageSizeFactorPx, channels, imageTransformationCodeOrNull,
-                            rangeOrNull);
+                            rangesOrNull);
                 }
             };
         ChannelWidgetWithListener widgetWithListener = new ChannelWidgetWithListener(viewerFactory);
         widgetWithListener.selectionChanged(channelChooser.getSelectedValues(),
                 channelChooser.tryGetSelectedTransformationCode(false),
-                channelChooser.tryGetSelectedIntensityRange());
+                channelChooser.tryGetSelectedIntensityRanges());
 
         ImageDatasetParameters imageParameters = image.tryGetImageDataset().getImageParameters();
         channelChooser.addSelectionChangedListener(widgetWithListener);
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/UserDefinedRescalingSettingsDialog.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/UserDefinedRescalingSettingsDialog.java
index 654640cb3a7..dca1d7a9e45 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/UserDefinedRescalingSettingsDialog.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/UserDefinedRescalingSettingsDialog.java
@@ -16,6 +16,9 @@
 
 package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import com.extjs.gxt.ui.client.event.BaseEvent;
@@ -27,6 +30,7 @@ import com.extjs.gxt.ui.client.widget.button.Button;
 import com.google.gwt.user.client.ui.Grid;
 
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.field.IntegerField;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget.LabeledItem;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IMessageProvider;
 import ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.Dict;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.IntensityRange;
@@ -36,46 +40,77 @@ import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.IntensityRange
  */
 public class UserDefinedRescalingSettingsDialog extends Dialog
 {
-    private final Label labelMin;
+    private static class SingleChannelIntesityRange
+    {
+        private final LabeledItem<String> channelCode;
+
+        private final Label labelMin;
+
+        private final Label labelMax;
+
+        private final IntegerField minTextField;
 
-    private final Label labelMax;
+        private final IntegerField maxTextField;
 
-    private final IntegerField minTextField;
+        public SingleChannelIntesityRange(IMessageProvider messageProvider,
+                LabeledItem<String> channelCode)
+        {
+            this.channelCode = channelCode;
+            labelMin =
+                    new Label(messageProvider.getMessage(Dict.RESCALING_DIALOG_MIN,
+                            channelCode.getLabel()));
+            labelMax =
+                    new Label(messageProvider.getMessage(Dict.RESCALING_DIALOG_MAX,
+                            channelCode.getLabel()));
+            minTextField =
+                    new IntegerField(messageProvider.getMessage(Dict.RESCALING_DIALOG_MIN,
+                            channelCode), true);
+            maxTextField =
+                    new IntegerField(messageProvider.getMessage(Dict.RESCALING_DIALOG_MAX,
+                            channelCode), true);
+        }
+    }
 
-    private final IntegerField maxTextField;
+    private final List<SingleChannelIntesityRange> intensitiesPerChannel;
 
     private final IMessageProvider messageProvider;
 
     private final IDefaultChannelState defaultChannelState;
 
-    private final String channelCode;
+    private final List<LabeledItem<String>> channelCodes;
 
     public UserDefinedRescalingSettingsDialog(IMessageProvider messageProvider,
             Map<String, IntensityRange> intensitiesPerChannel,
-            IDefaultChannelState defaultChannelState, String channelCode)
+            IDefaultChannelState defaultChannelState, List<LabeledItem<String>> channelCodes)
     {
         super();
 
         this.messageProvider = messageProvider;
         this.defaultChannelState = defaultChannelState;
-        this.channelCode = channelCode;
+        this.channelCodes = channelCodes;
 
         setHeading(this.messageProvider.getMessage(Dict.TITLE_USER_DEFINED_RESCALING_DIALOG));
         setButtons(Dialog.OKCANCEL);
-        labelMin = new Label(this.messageProvider.getMessage(Dict.RESCALING_DIALOG_MIN));
-        labelMax = new Label(this.messageProvider.getMessage(Dict.RESCALING_DIALOG_MAX));
-        minTextField =
-                new IntegerField(messageProvider.getMessage(Dict.RESCALING_DIALOG_MIN), true);
-        maxTextField =
-                new IntegerField(messageProvider.getMessage(Dict.RESCALING_DIALOG_MAX), true);
+
+        this.intensitiesPerChannel = new ArrayList<SingleChannelIntesityRange>(channelCodes.size());
+        for (LabeledItem<String> channelCode : channelCodes)
+        {
+            this.intensitiesPerChannel.add(new SingleChannelIntesityRange(messageProvider,
+                    channelCode));
+        }
 
         setInitialValues(intensitiesPerChannel);
 
-        Grid grid = new Grid(2, 2);
-        grid.setWidget(0, 0, labelMin);
-        grid.setWidget(0, 1, minTextField);
-        grid.setWidget(1, 0, labelMax);
-        grid.setWidget(1, 1, maxTextField);
+        Grid grid = new Grid(channelCodes.size() * 2, 2);
+        int counter = 0;
+        for (SingleChannelIntesityRange scir : this.intensitiesPerChannel)
+        {
+            grid.setWidget(2 * counter, 0, scir.labelMin);
+            grid.setWidget(2 * counter, 1, scir.minTextField);
+            grid.setWidget(2 * counter + 1, 0, scir.labelMax);
+            grid.setWidget(2 * counter + 1, 1, scir.maxTextField);
+            counter++;
+        }
 
         add(grid);
 
@@ -94,14 +129,17 @@ public class UserDefinedRescalingSettingsDialog extends Dialog
 
     private void setInitialValues(Map<String, IntensityRange> intensitiesPerChannel)
     {
-        minTextField.setValue(0);
-        maxTextField.setValue(65535);
-
-        IntensityRange range = intensitiesPerChannel.get(channelCode);
-        if (range != null)
+        for (SingleChannelIntesityRange scir : this.intensitiesPerChannel)
         {
-            minTextField.setValue(range.getBlackPoint());
-            maxTextField.setValue(range.getWhitePoint());
+            scir.minTextField.setValue(0);
+            scir.maxTextField.setValue(65535);
+
+            IntensityRange range = intensitiesPerChannel.get(scir.channelCode.getItem());
+            if (range != null)
+            {
+                scir.minTextField.setValue(range.getBlackPoint());
+                scir.maxTextField.setValue(range.getWhitePoint());
+            }
         }
     }
 
@@ -111,7 +149,7 @@ public class UserDefinedRescalingSettingsDialog extends Dialog
         if (button.getItemId().equals(Dialog.OK))
         {
             button.disable();
-            if (minTextField.isValid() && maxTextField.isValid())
+            if (areIntensitiesValid())
             {
                 super.onButtonPressed(button);
                 updateIntensityRescaling();
@@ -128,23 +166,40 @@ public class UserDefinedRescalingSettingsDialog extends Dialog
         }
     }
 
+    private boolean areIntensitiesValid()
+    {
+        for (SingleChannelIntesityRange scir : intensitiesPerChannel)
+        {
+            if ((false == scir.minTextField.isValid()) || (false == scir.maxTextField.isValid()))
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
     public void updateIntensityRescaling()
     {
-        IntensityRange result = null;
+        Map<String, IntensityRange> result = new HashMap<String, IntensityRange>();
 
         try
         {
-            int min = Integer.parseInt(minTextField.getValue().toString());
-            int max = Integer.parseInt(maxTextField.getValue().toString());
+            for (SingleChannelIntesityRange scir : intensitiesPerChannel)
+            {
+                int min = Integer.parseInt(scir.minTextField.getValue().toString());
+                int max = Integer.parseInt(scir.maxTextField.getValue().toString());
 
-            result = new IntensityRange(min, max);
-        } catch (NumberFormatException e)
-        {
-        }
+                result.put(scir.channelCode.getItem(), new IntensityRange(min, max));
+            }
 
-        if (result != null)
+            for (LabeledItem<String> channelCode : channelCodes)
+            {
+                defaultChannelState.setIntensityRange(channelCode.getItem(),
+                        result.get(channelCode.getItem()));
+            }
+        } catch (NumberFormatException e)
         {
-            defaultChannelState.setIntensityRange(channelCode, result);
         }
     }
 }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/WellContentDialog.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/WellContentDialog.java
index 519c872bd93..e7ecaddeaf6 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/WellContentDialog.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/WellContentDialog.java
@@ -18,6 +18,7 @@ package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import com.extjs.gxt.ui.client.Style.HorizontalAlignment;
 import com.extjs.gxt.ui.client.Style.Scroll;
@@ -172,7 +173,7 @@ public class WellContentDialog extends ImageDialog
     public static Widget createImageViewerForChannel(
             final IViewContext<IScreeningClientServiceAsync> viewContext,
             final WellContent wellImage, int imageSizePx, List<String> channels,
-            String imageTransformationCodeOrNull, IntensityRange rangeOrNull)
+            String imageTransformationCodeOrNull, Map<String, IntensityRange> rangesOrNull)
     {
         final ImageDatasetEnrichedReference imageDataset = tryGetImageDataset(wellImage);
         if (imageDataset == null)
@@ -210,7 +211,7 @@ public class WellContentDialog extends ImageDialog
                 new LogicalImageReference(imageDataset, locationOrNull);
         LogicalImageChannelsReference channelReferences =
                 LogicalImageChannelsReference.createWithoutOverlays(wellImages, channels,
-                        imageTransformationCodeOrNull, rangeOrNull);
+                        imageTransformationCodeOrNull, rangesOrNull);
         LayoutContainer staticTilesGrid =
                 LogicalImageViewer.createTilesGrid(channelReferences, sessionId, imageSizePx,
                         clickHandler, null);
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/WellSearchGrid.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/WellSearchGrid.java
index 76abc4000ed..961bd58e11d 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/WellSearchGrid.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/WellSearchGrid.java
@@ -19,6 +19,7 @@ package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import com.extjs.gxt.ui.client.store.ListStore;
@@ -639,18 +640,18 @@ public class WellSearchGrid extends TypedTableGrid<WellContent> implements
                                     @Override
                                     public Widget create(List<String> channels,
                                             String imageTransformationCodeOrNull,
-                                            IntensityRange rangeOrNull)
+                                            Map<String, IntensityRange> rangesOrNull)
                                     {
                                         return WellContentDialog.createImageViewerForChannel(
                                                 getViewContext(), entity, IMAGE_SIZE_PX, channels,
-                                                imageTransformationCodeOrNull, rangeOrNull);
+                                                imageTransformationCodeOrNull, rangesOrNull);
                                     }
                                 };
                     ChannelWidgetWithListener widgetWithListener =
                             new ChannelWidgetWithListener(viewerFactory);
                     widgetWithListener.selectionChanged(channelChooser.getSelectedValues(),
                             channelChooser.tryGetSelectedTransformationCode(false),
-                            channelChooser.tryGetSelectedIntensityRange());
+                            channelChooser.tryGetSelectedIntensityRanges());
 
                     ImageDatasetParameters imageParameters = images.getImageParameters();
                     channelChooser.addSelectionChangedListener(widgetWithListener);
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/dto/LogicalImageChannelsReference.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/dto/LogicalImageChannelsReference.java
index 87034aefc2d..71e4f79a1e5 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/dto/LogicalImageChannelsReference.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/dto/LogicalImageChannelsReference.java
@@ -18,6 +18,7 @@ package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.
 
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.IntensityRange;
@@ -31,10 +32,10 @@ public class LogicalImageChannelsReference
 {
     public static LogicalImageChannelsReference createWithoutOverlays(
             LogicalImageReference basicImage, List<String> channels,
-            String imageTransformationCodeOrNull, IntensityRange rangeOrNull)
+            String imageTransformationCodeOrNull, Map<String, IntensityRange> rangesOrNull)
     {
         return new LogicalImageChannelsReference(basicImage, channels,
-                imageTransformationCodeOrNull, rangeOrNull, new HashSet<ImageDatasetChannel>());
+                imageTransformationCodeOrNull, rangesOrNull, new HashSet<ImageDatasetChannel>());
     }
 
     // ----
@@ -45,18 +46,18 @@ public class LogicalImageChannelsReference
 
     private final String imageTransformationCodeOrNull;
 
-    private final IntensityRange rangeOrNull;
+    private final Map<String, IntensityRange> rangesOrNull;
 
     private final Set<ImageDatasetChannel> overlayChannels;
 
     public LogicalImageChannelsReference(LogicalImageReference basicImage, List<String> channels,
-            String imageTransformationCodeOrNull, IntensityRange rangeOrNull,
+            String imageTransformationCodeOrNull, Map<String, IntensityRange> rangesOrNull,
             Set<ImageDatasetChannel> overlayChannels)
     {
         this.basicImage = basicImage;
         this.channels = channels;
         this.imageTransformationCodeOrNull = imageTransformationCodeOrNull;
-        this.rangeOrNull = rangeOrNull;
+        this.rangesOrNull = rangesOrNull;
         this.overlayChannels = overlayChannels;
     }
 
@@ -80,8 +81,8 @@ public class LogicalImageChannelsReference
         return imageTransformationCodeOrNull;
     }
 
-    public IntensityRange tryGetIntensityRange()
+    public Map<String, IntensityRange> tryGetIntensityRanges()
     {
-        return rangeOrNull;
+        return rangesOrNull;
     }
 }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/image/Image.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/image/Image.java
index 27b95c867b8..099f9b31fac 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/image/Image.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/image/Image.java
@@ -17,6 +17,7 @@
 package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers.image;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import com.extjs.gxt.ui.client.widget.LayoutContainer;
@@ -62,20 +63,46 @@ public class Image extends LayoutContainer
 
         if (getInitializer().getChannelReferences().tryGetImageTransformationCode() != null)
         {
-            String suffix = "";
             if (ScreeningConstants.USER_DEFINED_RESCALING_CODE.equalsIgnoreCase(getInitializer()
                     .getChannelReferences().tryGetImageTransformationCode()))
             {
-                IntensityRange range =
-                        getInitializer().getChannelReferences().tryGetIntensityRange();
-                if (range != null)
+                Map<String, IntensityRange> rangesOrNull =
+                        getInitializer().getChannelReferences().tryGetIntensityRanges();
+                if (rangesOrNull != null)
                 {
-                    suffix = "(" + range.getBlackPoint() + "," + range.getWhitePoint() + ")";
+                    for (Map.Entry<String, IntensityRange> range : rangesOrNull.entrySet())
+                    {
+                        String suffix = "";
+                        if (range.getValue() != null)
+                        {
+                            suffix =
+                                    "(" + range.getValue().getBlackPoint() + ","
+                                            + range.getValue().getWhitePoint() + ")";
+                        }
+
+                        if (rangesOrNull.size() == 1)
+                        {
+                            url.addParameter(
+                                    ImageServletUrlParameters.SINGLE_CHANNEL_TRANSFORMATION_CODE_PARAM,
+                                    getInitializer().getChannelReferences()
+                                            .tryGetImageTransformationCode() + suffix);
+                        } else
+                        {
+                            url.addParameter(
+                                    ImageServletUrlParameters.SINGLE_CHANNEL_TRANSFORMATION_CODE_PARAM
+                                            + range.getKey(), getInitializer()
+                                            .getChannelReferences().tryGetImageTransformationCode()
+                                            + suffix);
+                        }
+
+                    }
                 }
+            } else
+            {
+                url.addParameter(
+                        ImageServletUrlParameters.SINGLE_CHANNEL_TRANSFORMATION_CODE_PARAM,
+                        getInitializer().getChannelReferences().tryGetImageTransformationCode());
             }
-            url.addParameter(ImageServletUrlParameters.SINGLE_CHANNEL_TRANSFORMATION_CODE_PARAM,
-                    getInitializer().getChannelReferences().tryGetImageTransformationCode()
-                            + suffix);
         }
 
         addImageTransformerSignature(url, getInitializer().getChannelReferences());
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/public/screening-dictionary.js b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/public/screening-dictionary.js
index b47d96e6c8f..24f0567e29e 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/public/screening-dictionary.js
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/public/screening-dictionary.js
@@ -142,8 +142,8 @@ RESOLUTION_CHOOSER_DEFAULT: "Default",
 RESOLUTION_CHOOSER_RESOLUTION: "{0}x{1}",
 
 TITLE_USER_DEFINED_RESCALING_DIALOG: "Set rescaling parameters",
-RESCALING_DIALOG_MIN: "black point: ",
-RESCALING_DIALOG_MAX: "white point: ",
+RESCALING_DIALOG_MIN: "{0} black point: ",
+RESCALING_DIALOG_MAX: "{0} white point: ",
 
 // LAST LINE: KEEP IT AT THE END
 lastline: "" // we need a line without a comma
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/server/logic/HCSImageDatasetLoader.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/server/logic/HCSImageDatasetLoader.java
index e068c430b37..0942ad3d12c 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/server/logic/HCSImageDatasetLoader.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/server/logic/HCSImageDatasetLoader.java
@@ -303,6 +303,7 @@ class HCSImageDatasetLoader extends PlateDatasetLoader
         DataStore dataStore = externalData.getDataStore();
         DataSetType dataSetType = externalData.getDataSetType();
         String dataSetTypeCodeOrNull = dataSetType == null ? null : dataSetType.getCode();
+
         return new ImageDatasetReference(externalData.getCode(), dataSetTypeCodeOrNull,
                 getDataStoreUrlFromDataStore(dataStore), createPlateIdentifier(externalData),
                 createExperimentIdentifier(externalData), extractPlateGeometry(externalData),
diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/ImageSizeFeedingMaintenanceTaskTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/ImageSizeFeedingMaintenanceTaskTest.java
index 949f4a09471..4ae052c1388 100644
--- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/ImageSizeFeedingMaintenanceTaskTest.java
+++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/ImageSizeFeedingMaintenanceTaskTest.java
@@ -28,9 +28,9 @@ import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import ch.systemsx.cisd.openbis.common.io.hierarchical_content.api.IHierarchicalContent;
 import ch.systemsx.cisd.common.logging.BufferedAppender;
 import ch.systemsx.cisd.common.test.RecordingMatcher;
+import ch.systemsx.cisd.openbis.common.io.hierarchical_content.api.IHierarchicalContent;
 import ch.systemsx.cisd.openbis.dss.etl.dataaccess.IImagingQueryDAO;
 import ch.systemsx.cisd.openbis.dss.etl.dto.ImageTransfomationFactories;
 import ch.systemsx.cisd.openbis.dss.generic.server.images.dto.RequestedImageSize;
@@ -57,7 +57,7 @@ public class ImageSizeFeedingMaintenanceTaskTest extends AssertJUnit
         public MockAbsoluteImageReference(int width, int height)
         {
             super(null, null, null, null, new RequestedImageSize(null, false), null,
-                    new ImageTransfomationFactories(), null, null);
+                    new ImageTransfomationFactories(), null, null, null);
             this.width = width;
             this.height = height;
         }
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 ceac2fb6ed1..eec32f3828b 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
@@ -38,10 +38,10 @@ import ch.systemsx.cisd.base.exceptions.CheckedExceptionTunnel;
 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.openbis.common.io.FileBasedContentNode;
-import ch.systemsx.cisd.openbis.common.io.hierarchical_content.api.IHierarchicalContentNode;
 import ch.systemsx.cisd.imagereaders.ImageReaderConstants;
 import ch.systemsx.cisd.imagereaders.ImageReadersTestHelper;
+import ch.systemsx.cisd.openbis.common.io.FileBasedContentNode;
+import ch.systemsx.cisd.openbis.common.io.hierarchical_content.api.IHierarchicalContentNode;
 import ch.systemsx.cisd.openbis.dss.etl.AbsoluteImageReference;
 import ch.systemsx.cisd.openbis.dss.etl.IImagingDatasetLoader;
 import ch.systemsx.cisd.openbis.dss.etl.ImagingLoaderStrategyFactory;
@@ -136,7 +136,7 @@ public class ImageChannelsUtilsTest extends AssertJUnit
 
     private ImageTransformationParams createSingleChannelTransformationParams()
     {
-        return new ImageTransformationParams(true, false, null);
+        return new ImageTransformationParams(true, false, null, new HashMap<String, String>());
     }
 
     @Test
@@ -226,8 +226,10 @@ public class ImageChannelsUtilsTest extends AssertJUnit
             });
 
         BufferedImage image =
-                createImageChannelsUtils(null).calculateBufferedImage(imageRef,
-                        new ImageTransformationParams(true, false, transformationCode));
+                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));
 
@@ -247,7 +249,8 @@ 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, 0, 255), new ImageTransfomationFactories(), null, null,
+                "ch2");
     }
 
     private ImageChannelsUtils createImageChannelsUtils(Size thumbnailSizeOrNull)
diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/screening/server/DssServiceRpcScreeningTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/screening/server/DssServiceRpcScreeningTest.java
index 085064a35e8..e88cabc523c 100644
--- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/screening/server/DssServiceRpcScreeningTest.java
+++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/screening/server/DssServiceRpcScreeningTest.java
@@ -456,14 +456,14 @@ public class DssServiceRpcScreeningTest extends AssertJUnit
                                     new Location(1, 1)), thumbnailSize, null);
                     will(returnValue(new AbsoluteImageReference(image("img1.jpg"), "img1", null,
                             null, thumbnailSize, createBlueColor(),
-                            new ImageTransfomationFactories(), null, null)));
+                            new ImageTransfomationFactories(), null, null, CHANNEL_CODE)));
                     one(imageLoader).tryGetImage(
                             CHANNEL_CODE,
                             ImageChannelStackReference.createHCSFromLocations(new Location(3, 1),
                                     new Location(2, 1)), thumbnailSize, null);
                     will(returnValue(new AbsoluteImageReference(image("img1.gif"), "img1", null,
                             null, thumbnailSize, createBlueColor(),
-                            new ImageTransfomationFactories(), null, null)));
+                            new ImageTransfomationFactories(), null, null, CHANNEL_CODE)));
                 }
             });
     }
@@ -590,7 +590,7 @@ public class DssServiceRpcScreeningTest extends AssertJUnit
                                     new Location(1, 1)), thumbnailSize, null);
                     will(returnValue(new AbsoluteImageReference(image("img1.jpg"), "img1", null,
                             null, thumbnailSize, createBlueColor(),
-                            new ImageTransfomationFactories(), null, null)));
+                            new ImageTransfomationFactories(), null, null, CHANNEL_CODE)));
 
                     one(imageLoader).tryGetImage(
                             CHANNEL_CODE,
@@ -598,7 +598,7 @@ public class DssServiceRpcScreeningTest extends AssertJUnit
                                     new Location(2, 1)), thumbnailSize, null);
                     will(returnValue(new AbsoluteImageReference(image("img1.png"), "img1", null,
                             null, thumbnailSize, createBlueColor(),
-                            new ImageTransfomationFactories(), null, null)));
+                            new ImageTransfomationFactories(), null, null, CHANNEL_CODE)));
 
                 }
             });
-- 
GitLab