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; |

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]; |

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; end |
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:


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 try 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 catch end setappdata(hImage, 'inCallback',[]); % reenable this callback end |
The result of this dynamic automatic color-scaling can be seen below:

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…
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 replacinghAxes.CLim = CLim;
with
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.
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
to avoid the occasional zero-index attempt error.