Obtaining an image with the right contrast for your application is never easy. Whether it is for art, detection, recognition, or improving your own photographs, contrast adjustment is a common task. Industrial setups can adjust the lighting and camera parameters before image capture—but what about after the image has been captured? Histogram processing is one answer for computationally adjusting the contrast, and can also be applied to a number of problems involving color balance or tone transfer.

This three-part post focuses on different methods for post-processing an image to modify its underlying histogram, affecting contrast and color balance in some amazing ways. The first part introduces basic pixel operations and how they affect histograms, and then shows several schemes for how to stretch a histogram to cover the full range of available pixel values. The second part discusses how to spread out the pixel values evenly over some range, a process known as histogram equalization. The third part extends the idea of controllably modifying the pixel values so that the resulting histogram matches any arbitrary distribution.

To begin, we’ll look at the basics of working with image histograms using MATLAB and how to perform histogram stretching to modify contrast and color.

Histogram math

A histogram is the probability distribution of pixel values in an image. (For RGB images, the histogram is usually broken into three histograms of the three component channels.) Like any other distribution, histograms have simple mathematical rules. Two operations that affect the pixel values, and thus the histograms, will be used extensively through these posts:

1) Adding a value to all the pixels adds that amount to the histogram; visually, this shifts the histogram

2) Multiplying all the pixel values by a certain amount scales where the histogram data appears; visually, this stretches the histogram

histogram math

To plot the histogram of an image, there are several options. If you have the Image Processing Toolbox, the imhist function is the easiest to use, but only works on single color channels. For example, to plot the green channel of an image,

img = imread('Flowers.jpg');

flower green hist

If you don’t have the Toolbox or want to have greater control over the histogram data, you can compute the values directly using the hist function and plot using your desired methods:

img = imread('Flowers.jpg');
g = img(:,:,2);
bins = 0:1:255;
H = hist(g(:), bins);
plot(bins, H, 'linewidth',2, 'color', 'g');

flower green hist direct

This second example assumes that the image uses eight bits per color channel, or a maximum value of 2^8-1 = 255. The remainder of the examples in these posts will use the assumption that pixel values range from 0 to 255.

Histogram stretching

Contrast is a measure of how much the pixel brightness changes relative to the average brightness. A technique known as histogram stretching can be used to shift the pixel values to fill the entire brightness range, resulting in high contrast.

The first step is to find the pixel values that should get mapped to 0% and 100% brightness. Any real-world image has noise, however. To keep the noise from unduly influencing the stretching, an assumption is made: a small percentage of the brightest and darkest pixels are ignored, writing them off to sensor noise. If you have the Image Processing Toolbox (we’ll cover what you can do if you don’t have the toolbox in a moment), you can use

limits = stretchlim(img, tol);

to find the lower and upper limits of the pixels in image img so that a fraction, tol, will be ignored. (See the stretchlim documentation for other ways to call the function.)

Next, the imadjust function can apply the scaling:

img_adjusted = imadjust(img, limits, []);

The function actually shifts and scales the pixel values so that the limits are scaled between 0 and 100% (the default when [] is used as the third input argument).

img = imread('treeArt.png');
limits = stretchlim(img, 0.01);
img_adjusted = imadjust(img, limits, []);
imagesc(img_adjusted, [0,255]);
axis image;

histogram stretched

In order to understand and program some of the more advanced histogram-processing techniques, it will help to take a moment to better understand what is happening behind the scenes of these two functions. (Similarly: if you don’t have the Image Processing Toolbox, this is how you could perform the same operations.) The first step, finding the input pixel values which correspond to some percentile of pixel values, is an inverse look-up. We can start by calculating the histogram, then computing its cumulative distribution function (CDF):

bins = linspace(0,255,256);
H = hist(img(:), bins);
H(H==0) = eps(sum(H));
cdf = [0,cumsum(H)/sum(H)];

histogram CDF

There are several assumptions here. The first is that the image, img, is assumed to have pixels which could range in value from 0 to 255. The second is that the histogram, H, could have bins with 0 counts. The problem is that, in the next step, we’ll be doing an inverse look-up, which means that we need an invertible function. If H has zeros, then cumsum(H) will have at least two identical values and will not be invertible. Setting the zero-count values in H to eps(sum(H)) ensures that H does not have non-zero values but only has a trivial effect on the CDF. Using the CDF for the inverse look-up can be as simple as this:

h_low = interp1(cdf, [0,bins], pct);
h_high = interp1(cdf, [0,bins], 1-pct);

where pct is the percent of pixels to ignore. This will give similar limits as stretchlim.

Figure: Interpolating the CDF at pct = 0.05 to find the corresponding h_low and h_high pixel values.

The next step is to adjust the image values so that the h_low and h_high values are remapped to the 0% and 100% brightness levels. Using simple histogram math,

img_adjusted2 = uint8( (double(img) – h_low)/(h_high-h_low) * 255);

Note that this includes both a shift, double(img) – h_low, and a stretch, 1/(h_high-h_low) of the original histogram.

In general, histogram stretching tends to work well on images that have a poor image contrast to start with and which should have full contrast. You can limit the amount of stretching that occurs by not mapping h_low and h_high to the 0% and 100% brightness levels, but instead by only scaling them a certain amount.

Multi-channel Histogram Stretching

The histogram stretching that we’ve looked at so far has focused on single-channel images. What’s useful is that the same functions can be applied to multi-channel images, like RGB images, by applying the codes to each channel independently. MATLAB’s stretchlim and imadjust functions are both written to be able to take in RGB images in addition to single-channel images using the exact same code as we used earlier.

Again, if you don’t have the Image Processing Toolbox or want to use multi-channel images that don’t have three channels (for example, some microscopy or satellite imagery), you’ll need to take a few extra steps. For example, say you have a multi-channel image, img, and you plan to adjust each channel. Then you could consider using something like this:

img_adjusted = zeros(size(img), 'uint8');
for ch = 1:size(img,3)
     img_channel = img(:,:,ch);
     limits = stretchlim(img_channel, 0.01);
     img_adjusted(:,:,ch) = imadjust(img_channel, limits, []);

Variations on Histogram Stretching

The RGB color space is the de-facto standard for images. It doesn’t always result in the best processing, however, especially when the image is intended for human viewing. A color space that better represents the human visual system, like L*a*b* or L’u’v’ can provide more natural stretching in some cases. In both of these color spaces, the L channel represents the brightness, while the (a*, b*) or (u’, v’) channels represent the color. (This is conceptually similar to YCbCr, used in JPEG encoding.) The stretching is performed on the L channel after converting the colors. Using the functions in the Image Processing Toolbox,

img = imread('lion.png');
c_rgb2lab = makecform('srgb2lab');
labimg = applycform(img, c_rgb2lab);
labimg(:,:,1) = imadjust(labimg(:,:,1), ...
stretchlim(labimg(:,:,1), 0.01) );
c_lab2rgb = makecform('lab2srgb');
img_adjusted = applycform(labimg, c_lab2rgb);

hist stretching w lab lionFigure: Histogram stretching within a L*a*b* color space and in an RGB color space. Note that the RGB stretching changes the colors slightly, emphasizing reds and yellows in this particular case.

Since only the L channel was adjusted, the colors remain the same, but the brightness is re-mapped. This adjusts the contrast in a way that sometimes can be more visually pleasing without significantly affecting the color balance.

If you don’t have the Image Processing Toolbox, Mark Ruzon wrote simple functions to convert between RGB and L*a*b*: Lab2RGB  and RGB2Lab.

Another variation is to apply histogram stretching in the hue-saturation-value (HSV) color space. The histogram stretching is only applied to the S and V channels, but not the hue channel. This tends to result in reasonable contrast and fuller colors without significantly affecting the color balance.

img = imread('bopFlower.png');
hsvimg = rgb2hsv(img);
for ch=2:3
      hsvimg(:,:,ch) = imadjust(hsvimg(:,:,ch), ...
      stretchlim(hsvimg(:,:,ch), 0.01));
img_adjusted = hsv2rgb(hsvimg);

bopFlower hsv stretchedStretching on HSV is also sometimes done after stretching in the RGB color space to ensure full color saturation. One example is this underwater image, which required significant stretching in RGB, but then benefits from an additional HSV stretch to gain full contrast:

ocean RGB and HSV


This is a good point to try out histogram stretching on your own images and get familiar with the different color transforms. In the next post, we’ll explore histogram equalization, primarily used for modifying the contrast in an image, and some of its extensions.