Search code examples
pythonopencvimage-processingcomputer-visionbinary-image

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)

Solution

  • 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.