Auto-scale image colors

I deal extensively in image processing in one of my consulting projects. The images are such that most of the interesting features are found in the central portion of the image. However, the margins of the image contain z-values that, while not interesting from an operational point-of-view, cause the displayed image’s color-limits (axes CLim property) to go wild. An image is worth a thousand words, so check the following raw image (courtesy of Flightware, Inc.), displayed by the following simple script:

hImage = imagesc(imageData); colormap(gray); colorbar;

Raw image with default Matlab CLim

Raw image with default Matlab CLim

Rescaling the axes color-limits

As you can see, this image is pretty useless for human-eye analysis. The reason is that while all of the interesting features in the central portion of the image have a z-value of ~-6, the few pixels in the margins that have a z-value of 350+ screw up the color limits and ruin the perceptual resolution (image contrast). We could of course start to guess (or histogram the z-values) to get the interesting color-limit range, and then manually set hAxes.CLim to get a much more usable image:

hAxes = hImage.Parent; hAxes.CLim = [-7.5,-6];

Raw image with a custom CLim

Raw image with a custom CLim

Auto-scaling the axes color-limits

Since the z-values range and distribution changes between different images, it would be better to automatically scale the axes color-limits based on an analysis of the image. A very simple technique for doing this is to take the 5%,95% or 10%,90% percentiles of the data, clamping all outlier data pixels to the extreme colors. If you have the Stats Toolbox you can use the prctile function for this, but if not (or even if you do), here’s a very fast alternative that automatically scales the axes color limits based on the specified threshold (a fraction between 0-0.49):

% Rescale axes CLim based on displayed image portion's CData
function rescaleAxesClim(hImage, threshold)
    % Get the displayed image portion's CData
    CData = hImage.CData;
    hAxes = hImage.Parent;
    XLim = fix(hAxes.XLim);
    YLim = fix(hAxes.YLim);
    rows = min(max(min(YLim):max(YLim),1),size(CData,1)); % visible portion
    cols = min(max(min(XLim):max(XLim),1),size(CData,2)); % visible portion
    CData = CData(unique(rows),unique(cols));
    CData = CData(:);  % it's easier to work with a 1d array
    % Find the CLims from this displayed portion's CData
    CData = sort(CData(~isnan(CData)));  % or use the Stat Toolbox's prctile()
    thresholdVals = [threshold, 1-threshold];
    thresholdIdxs = fix(numel(CData) .* thresholdVals);
    CLim = CData(thresholdIdxs);
    % Update the axes
    hAxes.CLim = CLim;

Note that a threshold of 0 uses the full color range, resulting in no CLim rescaling at all. At the other extreme, a threshold approaching 0.5 reduces the color-range to a single value, basically reducing the image to an unusable B/W (rather than grayscale) image. Different images might require different thresholds for optimal contrast. I believe that a good starting point for the threshold is a value of 0.10, which corresponds to the 10-90% range of CData values.

Dynamic auto-scaling of axes color-limits

This is very nice for the initial image display, but if we zoom-in, or pan a sub-image around, or update the image in some way, we would need to repeat calling this rescaleAxesClim() function every time the displayed image portion changes, otherwise we might still get unusable images. For example, if we zoom into the image above, we will see that the color-limits that were useful for the full image are much less useful on the local sub-image scale. The first (left) image uses the static custom color limits [-7.5,-6] above (i.e., simply zooming-in on that image, without modifying CLim again); the second (right) image is the result of repeating the call to rescaleAxesClim(), which improves the image contrast:

Zoomed-in image with a custom static CLim

Zoomed-in image with a custom static CLim

Zoomed-in image with a re-applied custom CLim

Zoomed-in image with a re-applied custom CLim

We could in theory attach the rescaleAxesClim() function as a callback to the zoom and pan functions (that provide such callback hooks). However, we would still need to remember to manually call this function whenever we modify the image or its containing axes programmatically.

A much simpler way is to attach our rescaleAxesClim() function as a callback to the image’s undocumented MarkedClean event:

% Instrument image: add a listener callback to rescale upon any image update
addlistener(hImage, 'MarkedClean', @(h,e)rescaleAxesClim(hImage,threshold));

In order to avoid callback recursion (potentially caused by modifying the axes CLim within the callback), we need to add a bit of code to the callback that prevents recursion/reentrancy (details). Here’s one simple way to do this:

% Rescale axes CLim based on displayed image portion's CData
function rescaleAxesClim(hImage, threshold)
    % Check for callback reentrancy
    inCallback = getappdata(hImage, 'inCallback');
    if ~isempty(inCallback), return, end
        setappdata(hImage, 'inCallback',1);  % prevent reentrancy
        % Get the displayed image portion's CData
        ...  (copied from above)
        % Update the axes
        hAx.CLim = CLim;
        drawnow; pause(0.001);  % finish all graphic updates before proceeding
    setappdata(hImage, 'inCallback',[]);  % reenable this callback

The result of this dynamic automatic color-scaling can be seen below:

Zoomed-in image with dynamic CLim

Zoomed-in image with dynamic CLim

autoScaleImageCLim utility

I have created a small utility called autoScaleImageCLim, which includes all the above, and automatically sets the specified input image(s) to use auto color scaling. Feel free to download this utility from the Matlab File Exchange. Here are a few usage examples:

autoScaleImageCLim()           % auto-scale the current axes' image
autoScaleImageCLim(hImage,5)   % auto-scale image using 5%-95% CData limits
autoScaleImageCLim(hImage,.07) % auto-scale image using 7%-93% CData limits

(note that the hImage input parameter can be an array of image handles)

Hopefully one day the so-useful MarkedClean event will become a documented and fully-supported event for all HG objects in Matlab, so that we won’t need to worry that it might not be supported by some future Matlab release…

Categories: Handle graphics, Listeners, Medium risk of breaking in future versions, Undocumented feature

Tags: , , ,

Bookmark and SharePrint Print

One Response to Auto-scale image colors

  1. Peter Cook says:

    Great utility. I do something quite similar in one of my applications. A couple remarks:

    1. If the user of this function has read your “Accelerating MATLAB Performance” book, it’s likely they have set the CLimMode of the parent axes and (if using a colorbar) the LimitsMode property of the colorbar axes peer to 'manual'. If the listener triggers the callback like this, the colorbar will display color ranges correctly but will look kind of funny (as it retains previous limits) so replacing

    hAxes.CLim = CLim;


    [hAxes.CLim, hCb.Limits] = deal(CLim);

    would be necessary.

    2. It may be the case that XData and YData are mapped to some values that don’t round nicely (like datetime/MATLAB serial datetime), so the fix() function will cause problems. There’s a couple cases to consider – in what you’ve shown, the XData and YData are mapped to integer image indices and this function works fine even though XData and YData are (1×2) arrays. A less desirable case would be mapped XData and YData, but with only corners specified so that XData and YData are (1×2) arrays, but are not integers and may not fix nicely. If an (mxn) CData array is mapped to (mx1) YData and (nx1) XData then logical masking works i.e.

    CData = hImage.CData(hImage.YData>=hAxes.YLim(1) & hImage.YData<=hAxes.YLim(2), ...
                         hImage.XData>=hAxes.XLim(1) & hImage.XData<=hAxes.XLim(2));
    CData = CData(:);

    3. For threshold values, depending on the dynamic range of the CData and zoom level the user has chosen, it may be safer to say

    thresholdIdx = [ceil(threshold*numel(CData)), floor((1-threshold)*numel(CData))]

    to avoid the occasional zero-index attempt error.

Leave a Reply

Your email address will not be published. Required fields are marked *