Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Histogram Equalization

stough 202-

Preview: Contrast and Image Enhancement

DIP book, exactly the content in 3.3.1 on Histogram Equalization.

At this point we’ve seen the usefulness of transfer functions for contrast enhancement in images, from simple linear windowing to nonliner transforms like power and log. We saw that in this context we look to construct transfer functions that dedicate more of the output range to the subset(s) of the input range where the image intensities are concentrated. In this notebook we’ll practically formulate the ideal contrast enhancing transfer function given any particular image.

Imports

Prior Example Transfer Functions

Here is a quick plot and discussion of some of the transfer functions covered in the enhance_transfer notebook.

<Figure size 600x400 with 1 Axes>

We saw previously that if our image has pixels that are all bunched up in color/intensity, leading to low contrast, our transfer function should accentuate those differences and thus improve contrast. We also saw how the histogram can tell us where to find these concentrations in the input domain.

Seen more practically then, a good transfer function has a slope above 1, leading to expansion and higher contrast, where the frequency of intensities is high in the input domain. To balance against that expansion though, the slope must end up less than 1, leading to compression of differences and reduction of contrast, in parts of the input domain that are sparsely populated.

As a result, we saw how an overexposed image, with many bright intensities and few dark ones, could be improved with for example an exponential (2x2^{x}). Similarly an underexposed image, with large dark areas, could be improved by a loglog or square root transform (γ=1/2\gamma=1/2).

Make Gaussian and Uniform Random Images

See how the uniform has better contrast, see what that means in the histograms. We can see that higher contrast goes in hand with a uniform histogram.

<Figure size 900x300 with 2 Axes>
<Figure size 900x300 with 2 Axes>

Build a parametric function that would spread the data out in I_gauss.

If we look at the above Gaussian distribution, and imagine drawing the transfer function over it, we can see that towards the extremes, where there’s not much data, the curve should be flat. Further, towards the middle the curve should be steep, to represent the idea that we are taking small differences in the input and mapping them to larger differences in the output.

This ends up looking something like an S curve or sigmoid.

S(x)=11+e−xS(x) = \frac{1}{1 + e^{-x}}

But we need a sigmoid that is centered around .5 and steep enough to look like an s inside [0,1]. We can rescale to our [0,255][0,255] mapping afterwards.

S(x,xc,s)=11+e−s(x−xc)S(x, x_c, s) = \frac{1}{1 + e^{-s(x-x_c)}}
<Figure size 400x300 with 1 Axes>

Apply the function to I_gauss and see how it improves contrast.

Since the S-curve function will return floating point numbers, for display purposes we must stick to [0,1][0,1].

<Figure size 800x300 with 2 Axes>
<Figure size 400x400 with 1 Axes>
<Figure size 400x400 with 1 Axes>

We can accomplish the same thing empirically, through the cumulative distribution function or CDF on the histogram

A fundamental result from probability theory, if s=T(r)s = \mathbf{T}(r) for T\mathbf{T} continuous and differentiable:

ps(s)=pr(r)∣drds∣p_s(s) = p_r(r)\begin{vmatrix}{\frac{dr}{ds}}\end{vmatrix}

The CDF measures the probability that a random variable XX takes on a value less than or equal to xx:

P(X≤x)=∫0xpr(w)dwP(X \le x) = \intop_{0}^{x} p_r(w)dw

where prp_r is the probability distribution (read: histogram) of our random variable rr. So to transform into a uniform distribution, our transfer function should be:

s=T(r)=(L−1)∫0rpr(w)dws = \mathbf{T}(r) = (L-1) \intop_{0}^{r} p_r(w)dw

Another way of looking at this transfer function is that it transforms an input intensity to that intensity’s rank/percentile in the image (then scaled by the output max). For example, if a pixel is at least as bright as three-quarters of the image pixels in the input, then that pixel will have three-quarters of the max brightness in the output.

Lastly, you have likely run into a real-world analogy to the CDF, in the form of your standardized test scores. The CDF provides the conversion from your test score to your percentile ranking compared with all others who took the test.

Computing the CDF

This next cell is key step 1 in performing histogram equalization: We must compute the Cumulative Distribution Function (CDF), which once normalized is the transfer function we want to apply.

  • We use numpy.histogram to get frequency counts freq for each possible intensity. (In floating point data it’s not particularly different, it’s just that each bin will end up representing more than one discrete intensity).

  • We use numpy.cumsum to gives a rolling sum cdf of the frequency counts in freq. It’s just a list of all the intermediate sums on the way to the overall sum.

  • We normalize both freq and cdf. This happens to help for the purpose of visualizing the two together, but in the case of cdf we’ll see it also helps normalizing the output of the transfer function easier. We could just end up multiplying by our maximum brightness.

The slope of the CDF at any bin is now proportional to the number of pixels in that bin. And we know that in order to improve contrast in an image, we want the slope of our transfer function to be high in places where there are lots of pixels bunched up together in intensity. So there we have it: the CDF exhibits exactly the slope we need in order to improve contrast.

<Figure size 400x300 with 1 Axes>

You can see in the above that indeed that the CDF has a high slope where there is a lot of data, and a lower slope where there is little or no data. It’s coincidental that the sigmoid can come close to matching the CDF of a Gaussian.

Now for key step 2 in performing histogram equalization: We must apply the cdf (coming from the histogram) as our transfer function.

For this we’re going to use the scipy function factory scipy.interpolate.interp1d. With the input range x coordinates bins and the output y values from cdf, interp1d returns a function that: given an x′x' in the input range returns the value y′y' through interpolation.

<Figure size 800x300 with 2 Axes>
((100, 100, 3), dtype('float64'), np.float64(6.666666666666674e-05), np.float64(1.0))

Clearly the CDF does a decent job here of improving the contrast from the original. There’s also a bit of a matplotlib gotcha at work here: the histogram-equalized image is actually floating point in [0,1][0,1]. If we wanted to convert back to integer, we must remember to multiply by 256 before casting to uint8.

As an aside: In that we have constructed I_gauss to be uint8 type integer data, we don’t actually need the interp1d we use above. A pixel value in I_gauss can serve perfectly well to simply index the cdf array. However the interp1d approach is generalizable to other kinds of image data, so I leave it here.

Let’s see what this did to our histogram. We’ll plot together the original and equalized images’ histograms to observe the difference directly.

<Figure size 600x300 with 1 Axes>

Conclusion

As you can see in this abstract example, histogram equalization is an ideal transfer function if the goal is to enhance the contrast in the image by making the histogram uniform, or as uniform as possible.

And here we get to the end of a notebook without having seen the application to any “real” natural image the kind about which we humans are concerned. Oh well, I leave it to you.

 

🔨 Your Turn: Equalize a Real Image

Task: Apply histogram equalization to a natural image, such as ../dip_pics/underExposed.jpg or ../dip_pics/got_tooDark.jpg, and show the original and equalized images side by side along with their histograms. Then do it a second way that leaves the colors alone, and compare.

Guidance:

  • It’s likely you can simply rename I_gauss and initialize it to plt.imread(...) for some image, then re-run the CDF cells. Check with arr_info that you have uint8 data in [0,255][0,255] first.

  • Be careful though, as treating all channels as one distribution, as we’ve done here, can lead to the warping of color. This doesn’t have much of an effect in our example since the R,G,B channels all do come from the same Gaussian distribution. In natural images that’s less likely.

  • To address this, think of alternative colorspaces: equalize only a brightness channel, such as V in HSV or L in Lab, and leave the color channels alone. You’ll need a histogram and CDF of that channel’s own range rather than [0,255][0,255].

  • To check your work, the equalized histogram should be much flatter, and its CDF close to a straight line. You can also compare against skimage.exposure.equalize_hist.