MathJax 3

Saturday, 7 August 2021

RGB image histogram equalization in Python

In RGB images, a correlation exists between the properties of the colours, so to equalize an RGB image's histogram is not possible to directly apply the histogram equalization technique used for greyscale on each channel. The Colour's intensity needs to be isolated. That's why, in the standard method, the image is converted first to HSV/HSL or Lab colour space. After this step, histogram equalization is applied to the value channel, as if it was an 8 bits grayscale image. In this way, hue and saturation will remain untouched, and so the image's colour balance.

The following picture shows the original RGB image converted to HSV and its value channel histogram:
 

 

The picture below shows the equalized histogram and the effect of histogram equalization on the image:



The source code for this post:


from PIL import Image, ImageTk
import tkinter as tk

def hsv2gray(input_image):
   if input_image.mode != 'HSV':
      return None
   else:
      output_image = Image.new('L', (input_image.width,
                           input_image.height))

      for x in range(input_image.width):
         for y in range(input_image.height):
            pix = input_image.getpixel((x, y))
            pix = pix[2]
            output_image.putpixel((x,y), pix)
      return output_image
 
def histogram_equalization(input_image):
    if input_image.mode != 'L' and input_image.mode != 'P':
        return None
    else:
        HISTEQ = [0 for i in range(256)]
        SUM = 0.0
        HIST = histogram(input_image)
        for i in range(256):
            SUM = 0.0
            for j in range(i+1):
                SUM += HIST[j]
            HISTEQ[i] = int(255*SUM+.5)
        output_image = Image.new('L', (input_image.width,
                                        input_image.height))
        for y in range(input_image.height):
            for x in range(input_image.width):
                pix = input_image.getpixel((x, y))
                output_image.putpixel((x, y), HISTEQ[pix])            
        return HISTEQ, output_image
      
def histogram(input_image):
    if input_image.mode != 'L' and input_image.mode != 'P':
        return None
    else:
        IHIST = [0 for i in range(256)]
        HIST = [0 for i in range(256)]
        SUM = 0        
        for x in range(input_image.width):
            for y in range(input_image.height):
                pix = input_image.getpixel((x, y))
                IHIST[pix] = IHIST[pix]+1
                SUM += 1
        for i in range(256):
            HIST[i] = float(IHIST[i]/SUM)
            
    return HIST

def draw_histogram(canvas, IHIST, hist_w, hist_h, color, rangeT=(0,256,1), bins=False):
   '''
    canvas: the Tk canvas
    IHIST:  the histogram to draw
    hist_w; histogram width
    hist_h: histogram height
    color:  plot colour
    rangeT: tuple used for histogram steps
    bins:   if False draws a line instead of bars
   '''
   
   hist_max = max(IHIST)
   ## Normalize between 0 and hist_h
   for i in range(256):
      IHIST[i] = float(IHIST[i]/hist_max) * hist_h
   ## A bin is a bar of the histogram
   bin_w = round(float(hist_w/300))
   offset = hist_w + 20 ## where we draw the first
   prev_x = offset
   prev_y = hist_h
   for i in range(rangeT[0],rangeT[1],rangeT[2]):
      if bins:
         canvas.create_line(bin_w*i+offset, hist_h,
            bin_w*i+offset, hist_h-IHIST[i],fill=color)
      else:
         canvas.create_line(prev_x, prev_y,
            bin_w*i+offset, hist_h-IHIST[i],fill=color)
         prev_x = bin_w*i+offset
         prev_y =  hist_h-IHIST[i]


if __name__ == "__main__":
   root = tk.Tk()
   img = Image.open("retriver.png")

   width = img.width*2+20
   height = img.height+20
   root.title("RGBtoHSV Image - Value channel equalized histogram")
   root.geometry(f'{width}x{height}')  

   output_image = img.convert('HSV')

   values_image = hsv2gray(output_image)
   HISTEQ = histogram(values_image)
   ##HISTEQ, values_image = histogram_equalization(values_image)
   ##HISTEQ = histogram(values_image)
   
   output_image2 = Image.new('HSV', (img.width,
                                     img.height))
 
   for x in range(img.width):
      for y in range(img.height):
         pix = values_image.getpixel((x, y))
         hsv_pix = output_image.getpixel((x,y))
         output_image2.putpixel((x,y), (hsv_pix[0], hsv_pix[1], pix))
    
   hist_w = img.width
   hist_h = img.height
 
   if output_image!=None:
      output_image = ImageTk.PhotoImage(output_image2)

      canvas = tk.Canvas(root, width=width, height=height, bg="#ffffff")
      canvas.create_image(width/4-1, height/2-1, image=output_image, state="normal")
      
      draw_histogram(canvas, HISTEQ, hist_w, hist_h, "#000", bins=True)
            
      canvas.place(x=0, y=0)
      canvas.pack()

      root.mainloop()
   else:
      print("Input image must be in 'RGB' colour space")


I will not go through all the code because I discussed it in the previous posts. I want only to explain this new function used to extract the value channel from the HSV image:

 

 def hsv2gray(input_image):
  if input_image.mode != 'HSV':
     return None
  else:
     output_image = Image.new('L', (input_image.width,
                          input_image.height))

     for x in range(input_image.width):
        for y in range(input_image.height):
           pix = input_image.getpixel((x, y))
           pix = pix[2]
           output_image.putpixel((x,y), pix)
     return output_image

 

 the function makes sure it is receiving an HSV image:
  

if input_image.mode != 'HSV':
     return None"


if so, it creates a new one in grayscale that will use as output:

 

     output_image = Image.new('L', (input_image.width,
                          input_image.height))


then it loops over the input image's pixels and stores the value channel in the newly created image, which it is then returned:

 

     for x in range(input_image.width):
        for y in range(input_image.height):
           pix = input_image.getpixel((x, y))
           pix = pix[2]
           output_image.putpixel((x,y), pix)
     return output_image)

 

Histogram equalization it's a simple and powerful tool to improve the contrast of both colour and grayscale images. There are other histogram techniques at our disposal, like histogram matching, which we'll learn in the next post. Stay tuned.


All posts in this serie

No comments:

Post a Comment