MathJax 3

Monday, 16 August 2021

Histogram matching(specification) inplementation in Python


In today note, we will continue our exploration of histogram techniques by learning about histogram matching, also called histogram specification.

Histogram equalization is already a particular case of histogram matching, but with some substantial differences.
In histogram matching, we increase or decrease the contrast of an image using the inverse transform of a specified histogram. But where do we get the histogram from, you may ask?
We can either generate one or get one from another image that satisfies our requirements for the image which contrast we want to modify. 


Let's assume we want to increase the brightness of the image below:



this image is quite dark and lacks contrast. Now, let's consider the next one:



the one above it's brighter than the first image, so it must also have a histogram with a range of values higher when compared to the first.  Let's see a visual confirmation of that:



 

now we are sure that the histogram of the image on the right it's a good specification to match for the image on the left as it contains a wider range of high intensity values. Now, we have all we need for brightening our image by histogram matching.

Histogram matching steps

The first step is to equalize the desired histogram and the histogram of our image:

 

    HISTSPEC = [0 for i in range(256)]
    SUM = 0.0
   
    _, eq_im = histogram_equalization(input_image)

    
    for I in range(256):
        SUM = 0.0
        for J in range(256):
            SUM += SPEC[J] # SPEC is passed to the function
        HISTSPEC[I] = int(255*SUM+0.5)


then, we can proceed to find the inverse transformation of the desired histogram:


    INVHIST = [0 for i in range(256)] 
    for I in range(256):
        minval = abs(I-HISTSPEC[0])
        minj = 0
        for J in range(256):
            if (abs(I - HISTSPEC[J]) < minval):
                minval = abs(I - HISTSPEC[J])
                minj = J
            INVHIST[I] = minj

 

finally applying  this to our image:

 

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


let's check the results:




the black histogram is that of the original image, the blue one is that of the image used to get the match, and the orange is the histogram of the resulting image. We had a noticeable improvement. The dynamic range of the resulting image is a lot wider than it was in the original, with a consequent increased contrast and brightness. The image on the left is the resulting image.

Below, a side by side comparation of the original image and the same after the matching process:

 

 

The complete code for this post:

 

 from PIL import Image, ImageTk, ImageFilter
import tkinter as tk

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 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_matching(input_image, SPEC):
    HISTSPEC = [0 for i in range(256)]
    INVHIST = [0 for i in range(256)]
    SUM = 0.0
    ## Equalize input_image's histogram
    _, eq_im = histogram_equalization(input_image)

    ## Equalize the specified histogram
    for I in range(256):
        SUM = 0.0
        for J in range(I):
            SUM += SPEC[J]
        HISTSPEC[I] = int(255*SUM+0.5)
        
    ## Find the inverse transformation
    ## of the specified histogram     
    for I in range(256):
        minval = abs(I-HISTSPEC[0])
        minj = 0
        for J in range(256):
            if (abs(I - HISTSPEC[J]) < minval):
                minval = abs(I - HISTSPEC[J])
                minj = J
            INVHIST[I] = minj
            
    output_image = Image.new('L', (input_image.width,
                                   input_image.height))

    ## Apply the inverse transformation
    ## to the gray-level values of th pixels
    ## in the equalized image
    for x in range(input_image.width):
        for y in range(input_image.height):
            pix = eq_im.getpixel((x, y))
            output_image.putpixel((x, y),INVHIST[pix])

    return INVHIST,
output_image                
        
    
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()
    root.title("HISTOGRAM MATCHING")
    imgT = Image.open("retriver_gray.png")
    HISTS = histogram(imgT) ## specified histogram
    
    img = Image.open("rose.jpg") ## input image
    
    print(img.mode)
    width = img.width*2
    height = img.height
    root.geometry(f'{width}x{height}')
    
    HISTOR = histogram(img) ## input image's histogram
    HISTEQ, out_im = histogram_matching(img, HISTS)
    HISTEQ = histogram(out_im)
 
    if HISTEQ != None:
        hist_w = img.width
        hist_h = img.height

        hist_max = max(HISTEQ) ## get the max value
        ## Normalize between 0 and hist_h
        for i in range(256):
            HISTEQ[i] = float(HISTEQ[i]/hist_max) * hist_h
        
        output_im = ImageTk.PhotoImage(out_im)
     
        canvas = tk.Canvas(root, width=width, height=height, bg="#ffffff")
        canvas.create_image(width/4-1, height/2-1, image=output_im, state="normal")
        out_im.save("rose_match2.jpg")

        draw_histogram(canvas, HISTS, hist_w, hist_h, color='#08f',bins=True)
        draw_histogram(canvas, HISTOR, hist_w, hist_h, color='#000',bins=True)
        draw_histogram(canvas, HISTEQ, hist_w, hist_h, color='#f80',bins=True)
        
             
        canvas.place(x=0, y=0)
        canvas.pack()
        
        root.mainloop()
    else:
        print("Input image's mode must be 'L' or 'P'"\
              "Check https://pillow.readthedocs.io/en/"\
              "latest/handbook/concepts.html#concept-modes")


 If you enjoy this content and would like to see more of it, or if you wish to know about a specific algorithm, please comment and also share this series. If you haven't read the older posts, head for the first one at this link.

Thursday, 12 August 2021

How to fix the webcam not working with opencv on Windows 10


If my blog is not ranked higher than stackoverflow.com, a good chance exists that you have been reading this SO's thread before landing here. Well, if you landed on my blog, it signifies SO didn't help you. If so, then keep reading.

 I installed OpenCV for the first time about 15 years ago. Since I can remember,  using a webcam has always been the most tricky part of working with it. Don't get me wrong, OpenCV is a magnificent open-source library, really of a high standard. But when it comes to closed source drivers, it may suffer from some compatibility problems, as it happens with nearly all open-source software.
There are other times instead when the causes for malfunctioning webcam lye outside of OpenCV source code.

As you may know already, Windows 10 offers some privacy setting which enables the user to exclude any program from using the webcam.
The good news is that these settings might be all that is preventing your webcam from working.
Open Settings and hit Privacy. On the left pane, hit Camera, go in the main panel and scroll all down. If you followed along, you should see this:

 



Make sure the last switch is on. Now, close the Settings and try out your OpenCV code.
Everything works, right?

With no shame, I admit that I reinstalled OpenCV 4.3.5 several times before realizing that switch had been turned off by Windows, for who knows which reasons.

I hope this helped you.

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

Saturday, 31 July 2021

Colour image histogram in Python

If you have missed the previous post on image channel decomposition, follow this link and then come back here. 

Colour image histogram

We can have two main types of colour image histogram: luminance histogram and component histogram. The principle used is the same as for the grey-level histogram in both the types, with one main difference between them:  the first type is obtained from a grey-scale version of the colour image, while the second type, from the image's three separate channels. 

Component histogram

To plot the component histogram of a colour image, after decomposing the image's channels into three separate images, each one containing only the colour for each channel, we must convert these images to grayscale. This step should account for the difference in human perception between red, green and blue, not just average the three channels' intensity, as explained here.

In the following picture, we can see the result of the above procedure executed on a 24 bits RGB image:

 

clockwise from top left:original image, red, blue and green channel

 

 

Some problems with the contrast are soon very evident, especially with the blue channel.

Once we have the arrays of the intensity values for each channel, we can apply the same steps to each channel's intensity array, as we would do for a grayscale image. That would produce the histogram for each one of the image's channels, as we can see in the next picture:

 

 
 
 
 
Interpreting the histogram
 
By analyzing the histogram above, it's evident that what it seems, after a quick look, a pretty and well-taken picture, in reality, it's instead a poor quality one and with a lot of room for improvements.
The red and the blue's dynamic range, ad example, it's too low. A little better for the greens, but still not enough intensity values. As a consequence, the contrast of the image lacks variance, and hence the colours look washed-up. 
However, we can use for colour images the same contrast improvement technique we learned here, like histogram equalization, which we will learn how to implement in the next post, together with other histogram operations.
 
 
The full source code for colour histogram:
 
from PIL import Image, ImageTk
import tkinter as tk
import sys

def rgb2gray(input_image):
   if
input_image.mode != 'RGB':
       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 = int(pix[0]*0.299 +
                         pix[1]*0.587 +
                         pix[2]*0.114)
               output_image.putpixel((x,y), pix)
       return 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, bins=False):
   hist_max = max(IHIST)## get the max value
   ## 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/256))
   offset = hist_w + 20 ## where we draw the first
   prev_x = offset
   prev_y = hist_h
   for i in range(256):
      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]

def get_channel(input_image, channel_key):
   if input_image.mode != 'RGB':
       return None
   else:
      channels_indices = {'R':0 ,'G':1 ,'B':2}
      output_image = Image.new('RGB', (input_image.width,
                                      input_image.height))
      ch_idx = channels_indices[channel_key]
      for x in range(input_image.width):
         for y in range(input_image.height):
            pix = input_image.getpixel((x, y))
            pix = pix[ch_idx]
            if ch_idx == 0:
               output_image.putpixel((x,y), (pix,0,0))
            elif ch_idx == 1:
               output_image.putpixel((x,y), (0,pix,0))
            elif ch_idx == 2:
               output_image.putpixel((x,y), (0,0,pix))
                  
       return output_image

if __name__ == "__main__":
   root = tk.Tk()
   img = Image.open("retriver.png")
   if img == None:
      print("Error opening image file"),
      sys.exit(1)

   width = img.width*2+20
   height = img.height+20
   root.title("RGB Image histogram")
   root.geometry(f'{width}x{height}')
   
   output_R = get_channel(img, 'R')  
   output_G = get_channel(img, 'G')
   output_B = get_channel(img, 'B')

   gray_R = rgb2gray(output_R)
   gray_G = rgb2gray(output_G)
   gray_B = rgb2gray(output_B)
   
   IHISTR = histogram(gray_R)
   IHISTG = histogram(gray_G)
   IHISTB = histogram(gray_B)
   
   hist_w = img.width
   hist_h = img.height
   
   if output_R != None and output_G!= None and output_B!=None:
      output_R = ImageTk.PhotoImage(output_R)
      output_G = ImageTk.PhotoImage(output_G)
      output_B = ImageTk.PhotoImage(output_B)
      input_im = ImageTk.PhotoImage(img)

      canvas = tk.Canvas(root, width=width, height=height, bg="#ffffff")
      canvas.create_image(width/4-1, height/2-1, image=input_im, state="normal")

      draw_histogram(canvas, IHISTR, hist_w, hist_h, "#f00")
      draw_histogram(canvas, IHISTG, hist_w, hist_h, "#0f0")
      draw_histogram(canvas, IHISTB, hist_w, hist_h, "#00f")
      
      canvas.place(x=0, y=0)
      canvas.pack()

      root.mainloop()
   else:
        print("Input image must be in 'RGB' colour space")
 
 
The code has been already explained in the previous posts. But, if something is not clear, ask for it in the comment section.

Friday, 30 July 2021

RGB channel decomposition - Python implementation

In the previous post, we have seen histogram equalization for grayscale images. Before learning about colour image histogram, we must know how to separate the channels of an image to be able to process them separately. We will learn how to do that in this post.
 
For this, we will use an RGB image with a bit depth of 8 bits per channel. I wrote a simple function which returns a copy of the input image containing only the data for a single channel. It's possible to select the required channel using a key ('R', 'G' or 'B') passed as an argument to the function: 

 

def get_channel(input_image, channel_key):
   if input_image.mode != 'RGB':
       return None
   else:
      channels_indices = {'R':0 ,'G':1 ,'B':2}
      output_image = Image.new('RGB', (input_image.width,
                                          
                    input_image.height))
      ch_idx = channels_indices[channel_key]
      for x in range(input_image.width):
         for y in range(input_image.height):
            pix = input_image.getpixel((x, y))
            pix = pix[ch_idx]
            if ch_idx == 0:
               output_image.putpixel((x,y), (pix,0,0))
            elif ch_idx == 1:
               output_image.putpixel((x,y), (0,pix,0))
            elif ch_idx == 2:
               output_image.putpixel((x,y), (0,0,pix))
                  
       return output_image

 

the function above takes an input image and a key as arguments. The key is used to choose the relevant channel's index for the pixel's tuple. Let's go through the most relevant lines:

channels_indices dict to map keys to indices,

channels_indices = {'R':0 ,'G':1 ,'B':2} 

ch_idx gets 0, 1 or 2 based on channel_key value

ch_idx = channels_indices[channel_key] 

first we get the pixel's tuple, then we get the channel's value from it:  

pix = input_image.getpixel((x, y))
pix = pix[ch_idx]

we also use ch_idx to select the output channel:

 if ch_idx == 0:
   output_image.putpixel((x,y), (pix,0,0))
 elif ch_idx == 1:
   output_image.putpixel((x,y), (0,pix,0))
 elif ch_idx == 2:
   output_image.putpixel((x,y), (0,0,pix))

In the following picture we can see the original image with the three channels separated:

 


 

and this is the full code:

 

from PIL import Image, ImageTk
import tkinter as tk


def get_channel(input_image, channel_key):
   if input_image.mode != 'RGB':
       return None
   else:
      channels_indices = {'R':0 ,'G':1 ,'B':2}
      output_image = Image.new('RGB', (input_image.width,
                                      input_image.height))
      ch_idx = channels_indices[channel_key]
      for x in range(input_image.width):
         for y in range(input_image.height):
            pix = input_image.getpixel((x, y))
            pix = pix[ch_idx]
            if ch_idx == 0:
               output_image.putpixel((x,y), (pix,0,0))
            elif ch_idx == 1:
               output_image.putpixel((x,y), (0,pix,0))
            elif ch_idx == 2:
               output_image.putpixel((x,y), (0,0,pix))
                  
       return output_image

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

   width = img.width*2+20
   height = img.height*2+20
   root.title("RGB channel decomposition")
   root.geometry(f'{width}x{height}')

   output_R = get_channel(img, 'R')  
   output_G = get_channel(img, 'G')
   output_B = get_channel(img, 'B')
     
   hist_w = img.width
   hist_h = img.height
   
   if output_R != None and output_G!= None and output_B!=None:
      output_R = ImageTk.PhotoImage(output_R)
      output_G = ImageTk.PhotoImage(output_G)
      output_B = ImageTk.PhotoImage(output_B)
      input_im = ImageTk.PhotoImage(img)

      canvas = tk.Canvas(root, width=width, height=height, bg="#ffffff")
      canvas.create_image(width/4-1, height/4-1, image=input_im, state="normal")
      canvas.create_image(3*width/4, height/4-1, image=output_R, state="normal")
      canvas.create_image(width/4-1, 3*height/4-1, image=output_G, state="normal")
      canvas.create_image(3*width/4, 3*height/4-1, image=output_B, state="normal")
      
      canvas.place(x=0, y=0)
      canvas.pack()

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

 
 

Now we can learn how to plot an histogram for a colour image. Follow me in the next post to see how it is done and how it can help us.

Wednesday, 28 July 2021

Histogram equalization - python implementation

As I have mentioned in the previous post, it's possible to process an image's histogram to spread the distribution of grey level values. This procedure is called histogram equalization. By equalizing the histogram, we redistribute the image's grey level values uniformly so that the number of pixels at any value becomes almost equivalent. More formally, this technique gives a linear trend to the cumulative probability function associated with the image.

Suppose that we have an image f(x,y), and its histogram h(i). The cumulative distribution function of h(i) is as follow: 

 


it can be proved that the above transform makes y = c(i) to follow a uniform distribution. Thus, for a 256 gray-level image, the following equation defines the transformation to equalize its histogram:

 


 where n is the total number of pixels in the image.

 This operation increases the contrast of the image, which becomes much clear and filled with details.

 

Histogram equalization in Python

As a first thing, we need a function to compute the image's histogram, about which you can read in the previous post:

 

 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

 

then we can write our histogram equalization function:

 

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):
                SUM += HIST[j]
            HISTEQ[i] = int(255*SUM+0.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 

 


The function above computes the histogram of the input image. Then, from this histogram, then the cumulative histogram is obtained (first nested loop). Finally, a new image is created and populated using the grey levels pixel values stored by the cumulative histogram (second nested loop). This image is then returned together with the histogram.

Below we can see the original greyscale image and its histogram:


original image and its histogram

the next picture shows the image after histogram equalization and its new histogram:


Picture A

Even though the plot doesn't show a scale, it's easy to see how the grey level values of the pixels are now more spread than in the previous image. 

In the next picture we can see the plot of the cumulative histogram:

 

Picture B

 

The above histogram is that returned by  the  histogram_equalization  function.

If we compute and plot the above picture's cumulative histogram, we obtain the results shown in the next picture: 


Picture C

we can see how the resulting cumulative histogram approximates a uniformly distributed image.

Histogram equalization implementation in Python

To obtain the results visible in the above pictures, we only need the two fumctions given at the beginning of this post, but we need to alternate them in the following manner:

Picture A:

    HISTEQ, out_im = histogram_equalization(img)
    HISTEQ = histogram(out_im)

Picture B:

    HISTEQ, out_im = histogram_equalization(img)

Picture C:

    HISTEQ, out_im = histogram_equalization(img)
    HISTEQ, out_im = histogram_equalization(out_im)

 

the following is the complete code for this post:

 

from PIL import Image, ImageTk
import tkinter as tk

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 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 draw_histogram(canvas, IHIST, hist_w, hist_h):
        ## A bin is a bar of the histogram
        bin_w = round(float(hist_w/512))
        offset = hist_w + 20 ## where we draw the first bin
        for i in range(256):                                                                                                                                                   
            canvas.create_line(bin_w*i+offset, hist_h,
                               bin_w*i+offset, hist_h-IHIST[i])



if __name__=="__main__":
    root = tk.Tk()
    root.title("IMAGE GREY-LEVEL HISTOGRAM EQUALIZED")
    img = Image.open("retriver_gray.png")

    width = img.width*2
    height = img.height
    root.geometry(f'{width}x{height}')

    HISTEQ, out_im = histogram_equalization(img)
    HISTEQ = histogram(out_im)
    ## HISTEQ, out_im = histogram_equalization(out_im)
      

    if HISTEQ != None:
        hist_w = img.width
        hist_h = img.height

        hist_max = max(HISTEQ) ## get the max value
        ## Normalize between 0 and hist_h
        for i in range(256):
            HISTEQ[i] = float(HISTEQ[i]/hist_max) * hist_h
        
        output_im = ImageTk.PhotoImage(out_im)
     
        canvas = tk.Canvas(root, width=width, height=height, bg="#ffffff")
        canvas.create_image(width/4-1, height/2-1, image=output_im, state="normal")

        draw_histogram(canvas, HISTEQ, hist_w, hist_h)
             
        canvas.place(x=0, y=0)
        canvas.pack()
        
        root.mainloop()
    else:
        print("Input image's mode must be 'L' or 'P'"\
              "Check https://pillow.readthedocs.io/en/"\
              "latest/handbook/concepts.html#concept-modes")

 

That's all for now. I hope you're enjoying and finding interesting this series on image processing in Python. Please, comment, share and get in touch if you find any error or if you would like to request a specific algorithm. In the next post we will learn about more histogram operations and colour image's histogram. Stay tuned.

  

Monday, 19 July 2021

Grey-level image histogram - Pyhton implementation

As promised in the previous post, we will talk about the image's histogram. 


Plotting data obtained from images is helpful to study and understand them. One plot type that can be helpful for any image processing practitioner is the grey-level histogram. The grey-level histogram of an image gives us, as it's easy to deduce, the grey-level distribution of the pixels within the image. 

 

The histogram of an image is the set of numbers M, the value of which representing what percentage of the image is at that grey level. The following formula defines the histogram of an image:

 

hi =  ni/nt   for i = 0  to  (M-1)

 

in which ni is the number of pixels within the image at the ith grey level value and nt is the total number of pixels in the image. 

 

The following function implements a grey-level histogram for a greyscale 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)] ## histogram array
        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

 


 

The picture below shows the image and its grey-level histogram:


Histogram interpretation

Histogram interpretation deserves a tutorial of its own, especially if we want to talk also about colour images histogram.
For now, we should understand at least that by examining the histogram of an image is possible to detect some problem caused during its acquisition, as poor or excessive contrast, or a low dynamic range.
We should also be aware of the existence of some techniques involving histograms that can help us to improve our images, as histogram equalization, which we'll discuss in the next post.

The complete code: 

 

from PIL import Image, ImageTk
import tkinter as tk



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):
        ## A bin is a bar of the histogram
        bin_w = round(float(hist_w/512))
        offset = hist_w + 20 ## where we draw the first bin
        for i in range(256):                                                                                                                                                   
            canvas.create_line(bin_w*i+offset, hist_h,
                               bin_w*i+offset, hist_h-IHIST[i])


if __name__=="__main__":
    root = tk.Tk()
    root.title("IMAGE GREY-LEVEL HISTOGRAM")
    img = Image.open("retriver_gray.png")

    width = img.width*2
    height = img.height
    root.geometry(f'{width}x{height}')

    IHIST = histogram(img)
    if IHIST != None:
        hist_w = img.width
        hist_h = img.height

        hist_max = max(IHIST) ## get the max value
        ## Normalize between 0 and hist_h
        for i in range(256):
            IHIST[i] = float(IHIST[i]/hist_max) * hist_h
    
        input_im = ImageTk.PhotoImage(img)
     
        canvas = tk.Canvas(root, width=width, height=height, bg="#ffffff")
        canvas.create_image(width/4-1, height/2-1, image=input_im, state="normal")

        draw_histogram(canvas, IHIST, hist_w, hist_h)
             
        canvas.place(x=0, y=0)
        canvas.pack()
        
        root.mainloop()
    else:
        print("Input image's mode must be 'L' or 'P'"\
              "Check https://pillow.readthedocs.io/en/"\
              "latest/handbook/concepts.html#concept-modes")