MathJax 3

Showing posts with label histogram. Show all posts
Showing posts with label histogram. Show all posts

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.

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

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")