Joshua Yang
Joshua Yang

Reputation: 59

Trouble getting accurate binary image OpenCV

Using the threshold functions in open CV on an image to get a binary image, with Otsu's thresholding I get a image that has white spots due to different lighting conditions in parts of the imageenter image description here

or with adaptive threshold to fix the lighting conditions, it fails to accurately represent the pencil-filled bubbles that Otsu actually can represent.

enter image description here

How can I get both the filled bubbles represented and a fixed lighting conditions without patches? Here's the original image enter image description here

Here is my code

    #binary image conversion
    thresh2 = cv2.adaptiveThreshold(papergray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 21, 13)
    thresh = cv2.threshold(papergray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

    cv2.imshow("Binary", thresh) #Otsu's
    cv2.imshow("Adpative",thresh2)

Upvotes: 3

Views: 974

Answers (3)

Jeru Luke
Jeru Luke

Reputation: 21233

Problems with your approach:

The methods you have tried out:

  1. Otsu threshold is decided based on all the pixel values in the entire image (global technique). If you look at the bottom-left of your image, there is a gray shade which can have an adverse effect in deciding the threshold value.
  2. Adaptive threshold: here is a recent answer on why it isn't helpful. In short, it acts like an edge detector for smaller kernel sizes

What you can try:

OpenCV's ximgproc module has specialized binary image generation methods. One such method is the popular Niblack threshold technique.

This is a local threshold technique that depends on statistical measures. It divides the image into blocks (sub-images) of size predefined by the user. A threshold is set based on the mean minus k times standard deviation of pixel values for each block. The k is decided by the user.

Code:

img =cv2.imread('omr_sheet.jpg')
blur = cv2.GaussianBlur(img, (3, 3), 0)
gray = cv2.cvtColor(blur, cv2.COLOR_BGR2GRAY)
niblack = cv2.ximgproc.niBlackThreshold(gray, 255, cv2.THRESH_BINARY, 41, -0.1, binarizationMethod=cv2.ximgproc.BINARIZATION_NICK)

Result:

enter image description here

Links:

  1. To know more about cv2.ximgproc.niBlackThreshold

  2. There are other binarization techniques available that you may want to explore. It also contains links to research papers that explain each of these techniques on detail.

Edit: Adaptive threshold actually works if you know what you are working with. You can decide the kernel size beforehand.

See Prashant's answer.

Upvotes: 4

Cris Luengo
Cris Luengo

Reputation: 60770

An alternative approach would be to apply a morphological closing, which would remove all the drawing, yielding an estimate of the illumination level. Dividing the image by the illumination level gives you an image of the sheet corrected for illumination:

corrected for illumination

In this image we can easily apply a global threshold:

thresholded

I used the following code:

import diplib as dip

img = dip.ImageRead('tlCw6.jpg')(1)
corrected = img / dip.Closing(img, dip.SE(40, 'rectangular'))
out = dip.Threshold(corrected, method='triangle')[0]

Upvotes: 6

Prashant Maurya
Prashant Maurya

Reputation: 678

This can be done with cv2.ADAPTIVE_THRESH_MEAN_C:

import cv2
img = cv2.imread("omr.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 31, 10)
cv2.imshow("Mean Adaptive Thresholding", thresh)
cv2.waitKey(0)

The output is: enter image description here

Upvotes: 5

Related Questions