Contrast stretching in Python/ OpenCV

Question:

Searching Google for Histogram Equalization Python or Contrast Stretching Python I am directed to the same links from python documentation in OpenCv which are actually both related to equalization and not stretching (IMO).

  1. http://docs.opencv.org/2.4/doc/tutorials/imgproc/histograms/histogram_equalization/histogram_equalization.html

  2. http://docs.opencv.org/3.2.0/d5/daf/tutorial_py_histogram_equalization.html

Read the documentation, it seems to be a confusion with the wording, as it describes equalization as a stretching operation:

What Histogram Equalization does is to stretch out this range.

AND

So you need to stretch this histogram to either ends (as given in below image, from wikipedia) and that is what Histogram Equalization does (in simple words)

I feel that is wrong because nowhere on Wikipedia it says that histogram equalization means stretching, and reading other sources they clearly distinguish the two operations.

  1. http://homepages.inf.ed.ac.uk/rbf/HIPR2/histeq.htm
  2. http://homepages.inf.ed.ac.uk/rbf/HIPR2/stretch.htm

My questions:

  1. does the OpenCV documentation actually implements Histogram Equalization, while badly explaining it?

    1. Is there any implementation for contrast stretching in Python? (OpenCV, etc?)
Asked By: RMS

||

Answers:

OpenCV doesn’t have any function for contrast stretching and google yields the same result because histogram equalization does stretch the histogram horizontally but its just the difference of the transformation function. (Both methods increase the contrast of the images.Transformation function transfers the pixel intensity levels from the given range to required range.)

Histogram equalization derives the transformation function(TF) automatically from probability density function (PDF) of the given image where as in contrast stretching you specify your own TF based on the applications’ requirement.

One simple TF through which you can do contrast stretching is min-max contrast stretching –

((pixel – min) / (max – min))*255.

You do this for each pixel value. min and max being the minimum and maximum intensities.

Answered By: hashcode55

You can also use cv2.LUT for contrast stretching by creating a custom table using np.interp. Links to their documentation are this and this respectively. Below an example is shown.

import cv2
import numpy as np

img = cv2.imread('messi.jpg')
original = img.copy()
xp = [0, 64, 128, 192, 255]
fp = [0, 16, 128, 240, 255]
x = np.arange(256)
table = np.interp(x, xp, fp).astype('uint8')
img = cv2.LUT(img, table)
cv2.imshow("original", original)
cv2.imshow("Output", img)
cv2.waitKey(0)
cv2.destroyAllWindows() 

The table created

[  0   0   0   0   1   1   1   1   2   2   2   2   3   3   3   3   4   4
   4   4   5   5   5   5   6   6   6   6   7   7   7   7   8   8   8   8
   9   9   9   9  10  10  10  10  11  11  11  11  12  12  12  12  13  13
  13  13  14  14  14  14  15  15  15  15  16  17  19  21  23  24  26  28
  30  31  33  35  37  38  40  42  44  45  47  49  51  52  54  56  58  59
  61  63  65  66  68  70  72  73  75  77  79  80  82  84  86  87  89  91
  93  94  96  98 100 101 103 105 107 108 110 112 114 115 117 119 121 122
 124 126 128 129 131 133 135 136 138 140 142 143 145 147 149 150 152 154
 156 157 159 161 163 164 166 168 170 171 173 175 177 178 180 182 184 185
 187 189 191 192 194 196 198 199 201 203 205 206 208 210 212 213 215 217
 219 220 222 224 226 227 229 231 233 234 236 238 240 240 240 240 240 241
 241 241 241 242 242 242 242 243 243 243 243 244 244 244 244 245 245 245
 245 245 246 246 246 246 247 247 247 247 248 248 248 248 249 249 249 249
 250 250 250 250 250 251 251 251 251 252 252 252 252 253 253 253 253 254
 254 254 254 255]

Now cv2.LUT will replace the values of the original image with the values in the table. For example, all the pixels having values 1 will be replaced by 0 and all pixels having values 4 will be replaced by 1.

Original Image

Original

Contrast Stretched Image

enter image description here

The values of xp and fp can be varied to create custom tables as required and it will stretch the contrast even if min and max pixels are 0 and 255 unlike the answer provided by hashcode55.

Answered By: Vardan Agarwal

Python/OpenCV can do contrast stretching via the cv2.normalize() method using min_max normalization. For example:

Input:

enter image description here

#!/bin/python3.7

import cv2
import numpy as np

# read image
img = cv2.imread("zelda3_bm20_cm20.jpg", cv2.IMREAD_COLOR)

# normalize float versions
norm_img1 = cv2.normalize(img, None, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_32F)
norm_img2 = cv2.normalize(img, None, alpha=0, beta=1.2, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_32F)

# scale to uint8
norm_img1 = (255*norm_img1).astype(np.uint8)
norm_img2 = np.clip(norm_img2, 0, 1)
norm_img2 = (255*norm_img2).astype(np.uint8)

# write normalized output images
cv2.imwrite("zelda1_bm20_cm20_normalize1.jpg",norm_img1)
cv2.imwrite("zelda1_bm20_cm20_normalize2.jpg",norm_img2)

# display input and both output images
cv2.imshow('original',img)
cv2.imshow('normalized1',norm_img1)
cv2.imshow('normalized2',norm_img2)
cv2.waitKey(0)
cv2.destroyAllWindows()

Normalize1:

enter image description here

Normalize2:

enter image description here

You can also do your own stretching by using a simple linear equation with 2 pair of input/ouput values using the form y=A*x+B and solving the two simultaneous equations. See concept for stretching shown in How can I make the gradient appearance of one image equal to the other?

Answered By: fmw42

Ok, so I wrote this function that does Standard Deviation Contrast Stretching, on each band of an image.

For normal distributions, 68% of the observations lie within – 1 standard deviation of the mean, 95.4% of all observations lie within – 2 standard deviations, and 99.73% within – 3 standard deviations.

this is basically a min-max stretch but the max is mean+sigma*std and min is mean-sigma*std

def stretch(img,sigma =3,plot_hist=False):
    stretched = np.zeros(img.shape) 
    for i in range(img.shape[2]):  #looping through the bands
        band = img[:,:,i] # copiying each band into the variable `band`
        if np.min(band)<0: # if the min is less that zero, first we add min to all pixels so min becomes 0
            band = band + np.abs(np.min(band)) 
        band = band / np.max(band)
        band = band * 255 # convertaning values to 0-255 range
        if plot_hist:
            plt.hist(band.ravel(), bins=256) #calculating histogram
            plt.show()
        # plt.imshow(band)
        # plt.show()
        std = np.std(band)
        mean = np.mean(band)
        max = mean+(sigma*std)
        min = mean-(sigma*std)
        band = (band-min)/(max-min)
        band = band * 255
        # this streching cuases the values less than `mean-simga*std` to become negative
        # and values greater than `mean+simga*std` to become more than 255
        # so we clip the values ls 0 and gt 255
        band[band>255]=255  
        band[band<0]=0
        print('band',i,np.min(band),np.mean(band),np.std(band),np.max(band))
        if plot_hist:
            plt.hist(band.ravel(), bins=256) #calculating histogram
            plt.show()
        stretched[:,:,i] = band
    
    
stretched = stretched.astype('int')
return stretched

in the case above, I didn’t need the band ratios to stay the same, but the best practice for an RGB image would be like this:
https://docs.opencv.org/4.x/d5/daf/tutorial_py_histogram_equalization.html

Unfortunately, this CLAHE stretching does not work on multi-band images so should be applied to each band separately – which gives wrong results since the contrast between each band will be lost and the images tend to be gray. what we need to do is:
we need to transform the image into HSV color space and stretch the V (value – intensity) and leave the rest. this is how we get a good stretch(pun intended).

The thing about cv.COLOR_HSV2RGB is that it actually returns BGR instead of RGB so after the HSV2RGB we need to reverse the bands.

here’s the function I wrote:

def stack_3_channel(r,g,b , clipLimit = 20 ,  tileGridSize=(16,16) ):
  
  img = np.stack([r,g,b], axis=2)
  img = cv.normalize(img, None, 0, 255, cv.NORM_MINMAX, dtype=cv.CV_8U)

  hsv_img = cv.cvtColor(img, cv.COLOR_BGR2HSV)
  h, s, v = hsv_img[:,:,0], hsv_img[:,:,1], hsv_img[:,:,2]


  
  clahe = cv.createCLAHE(clipLimit, tileGridSize)
  v = clahe.apply(v) #stretched histogram for showing the image with better contrast - its not ok to use it for scientific calculations

  hsv_img = np.dstack((h,s,v))

  # NOTE: HSV2RGB returns BGR instead of RGB
  bgr_stretched = cv.cvtColor(hsv_img, cv.COLOR_HSV2RGB)

  #reversing the bands back to RGB
  rgb_stretched = np.zeros(bgr_stretched.shape)
  rgb_stretched[:,:,0] = bgr_stretched[:,:,2]
  rgb_stretched[:,:,1] = bgr_stretched[:,:,1]
  rgb_stretched[:,:,2] = bgr_stretched[:,:,0]

  # if the valuse are float, plt will have problem showing them
  rgb_stretched = rgb_stretched.astype('uint8')

  return img , rgb_stretched

image after histogram stretching

Answered By: moien rangzan