Python OpenCV not detecting obvious contours

Viewed 416

Apologies as I'm very new to OpenCV and the world of image processing in general.

I'm using OpenCV in Python to detect contours/boxes in this image.

Original

It almost manages to detect all contours, but for some odd reason it doesn't pick up the last row and column which are obvious contours. This image shows the bounding boxes for contours it manages to identify.

enter image description here

Not entirely sure why it's not able to easily pick up the remaining contours. I've researched similar questions but haven't found a suitable answer.

Here's my code.

import numpy as np
import cv2
import math
import matplotlib.pyplot as plt

#load image
img = cv2.imread(path)

#remove noise
img = cv2.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21)

#convert to gray scale
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

#make pixels darker
_, img = cv2.threshold(img, 240, 255, cv2.THRESH_TOZERO)
        
#thresholding the image to a binary image
thresh, img_bin = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

#inverting the image 
img_bin = 255 - img_bin

# countcol(width) of kernel as 100th of total width
kernel_len = np.array(img).shape[1]//100

# Defining a vertical kernel to detect all vertical lines of image 
ver_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, kernel_len))

# Defining a horizontal kernel to detect all horizontal lines of image
hor_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_len, 1))

# A kernel of 2x2
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))

#Use vertical kernel to detect and save the vertical lines in a jpg
image_1 = cv2.erode(img_bin, ver_kernel, iterations = 3)
vertical_lines = cv2.dilate(image_1, np.ones((10, 4),np.uint8), iterations = 30)
vertical_lines = cv2.erode(vertical_lines, np.ones((10, 4),np.uint8), iterations = 29)

#Use horizontal kernel to detect and save the horizontal lines in a jpg
image_2 = cv2.erode(img_bin, np.ones((1, 5),np.uint8), iterations = 5)
horizontal_lines = cv2.dilate(image_2, np.ones((2, 40),np.uint8), iterations = 20)
horizontal_lines = cv2.erode(horizontal_lines, np.ones((2, 39),np.uint8), iterations = 19)

# Combine horizontal and vertical lines in a new third image, with both having same weight.
img_vh = cv2.addWeighted(vertical_lines, 0.5, horizontal_lines, 0.5, 0.0)
rows, cols = img_vh.shape

#shift image so the enhanced lines overlap with original image
M = np.float32([[1,0,-30],[0,1,-21]])
img_vh = cv2.warpAffine(img_vh ,M,(cols,rows))

#Eroding and thesholding the image
img_vh = cv2.erode(~img_vh, kernel, iterations = 2)
thresh, img_vh = cv2.threshold(img_vh, 128, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

bitxor = cv2.bitwise_xor(img, img_vh)
bitnot = cv2.bitwise_not(bitxor)

#find contours
contours, _ = cv2.findContours(img_vh, cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)

#create list empty list to append with contours less than a specified area
new_contours = []

for contour in contours:
    if cv2.contourArea(contour) < 4000000:
        new_contours.append(contour)

#get bounding boxes
bounding_boxes = [cv2.boundingRect(contour) for contour in new_contours]

#plot detected bounding boxes
img_og = cv2.imread(path)
for bounding_box in bounding_boxes:
    x,y,w,h = bounding_box
    img_plot = cv2.rectangle(img_og, (x, y), (x+w, y+h), (255, 0, 0) , 2)

    plotting = plt.imshow(img_plot, cmap='gray')
    plt.show()
1 Answers

Like @ypnos was suggesting, the dilation and erosion has most likely pushed the last line off the image in the "saving horizontal lines" section. So the image_vh wouldn't have the last row when it was being searched for contours. I tested (Note:1) this by viewing the image after each of your transformations.

Specifically, the number of iterations had been too much. You had used a reasonably sized kernel as it is. It gave perfect results with iterations = 2 on lines 43 and 44 of your code.

After modifying them to :

horizontal_lines = cv2.dilate(image_2, np.ones((2, 40), np.uint8), iterations=2)
horizontal_lines = cv2.erode(horizontal_lines, np.ones((2, 39), np.uint8), iterations=2)

the bounding box rectangles had shifted off the image a bit. That was fixed by changing line 51 of the code to:

M = np.float32([[1, 0, -30], [0, 1, -5]])

This was the result. Image with bounding rectangles

Note:

  1. I test/debug using this function usually.
def test(image, title):
    cv2.imshow(title, image)
    cv2.waitKey(0)
    cv2.destroyWindow(title)

The variable position and the handy waitkey calms me down.

Related