Find countours on a dark background

Question:

Suppose, I have a dark image on a white background. For example, the image below:

enter image description here

With the code below, I can easily extract its countors

import imutils
import cv2

image = cv2.imread("image.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image = imutils.resize(image, width = 64)
thresh = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 11, 7)
# initialize the outline image, find the outermost
# contours then draw
# it
outline = np.zeros(image.shape, dtype = "uint8")
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[0]
x=cv2.drawContours(outline, [cnts], -1, 255, -1)

This would give me the image below

easy image, white background, good contrast between foreground and background

However, let us try a more difficult image, like below

difficult image

whenever I run such a code using the image above, I find the following error:

Traceback (most recent call last):
File "segment_img.py", line 16, in <module>
cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[0]
IndexError: list index out of range

It obviously does not find any countors. However, I am using adaptive threshold which is supposed to work.

What can I change in my code so I could make both images having their corresponding contours?

Asked By: mad

||

Answers:

Instead of using cv2.adaptiveThreshold we may use cv2.threshold with cv2.THRESH_OTSU argument (and cv2.THRESH_BINARY_INV):

thresh = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)[1]

cv2.THRESH_OTSU uses Otsu’s method for automatic image thresholding.


Code sample:

import imutils
import cv2
import numpy as np

image = cv2.imread("image.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

image = imutils.resize(image, width = 64)
#thresh = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 11, 7)
thresh = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)[1]
# initialize the outline image, find the outermost
# contours then draw
# it
outline = np.zeros(image.shape, dtype="uint8")
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
cnts = sorted(cnts, key=cv2.contourArea, reverse = True)[0]
x = cv2.drawContours(outline, [cnts], -1, 255, -1)

Result:
enter image description here


It’s not a magic that works in all cases, and it’s also possible to select parameters for cv2.adaptiveThreshold that works for the specific case.

Otsu’s method usually works where there are two main distinct gray levels (with some deviation from the mean of each distinct level).

Answered By: Rotem

The Otsu’s threshold did more or less what I need.

def prepare_image(image_name):

    image = cv2.imread(image_name)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    image=cv2.resize(image, (28,28), interpolation = cv2.INTER_AREA)

    ret2,thresh = v2.threshold(image,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)

    # initialize the outline image, find the outermost
    # contours, then draw
    # it
    outline = np.zeros(image.shape, dtype = "uint8")
    cnts = cv2.findContours(thresh.copy(), 
    cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
    cnts = imutils.grab_contours(cnts)
    cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[0]
    cv2.drawContours(outline, [cnts], -1, 255, -1)


    x=np.zeros(outline.shape)
    x[outline==0]=255

    return x

The outputs of the two images are more or less like these:

enter image description here
enter image description here

Not excactly perfect. I am open to other suggestions.

Answered By: mad