2

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()
8
  • 1
    What's the deal with #shift image so the enhanced lines overlap with original image? I would assume at that point you are already in trouble. Did your excessive amount of dilations and erosions drift away out of the image dimensions? Commented Jul 20, 2020 at 21:43
  • 1
    Have you looked at your thresholded image? Otsu thresholding may be losing the black lines for that row. Commented Jul 20, 2020 at 22:41
  • @ypnos Great question. I should have provided a little more background info. I'm working with various images that look identical though vary in overall image quality. Some that are considerably bad have "gaps" in vertical and horizontal lines which makes finding contours difficult. Dilating lines as I have fills in gaps, but shifts the lines slightly and they don't align with the original, hence why I added the portion to shift the image. Commented Jul 20, 2020 at 23:15
  • 1
    Have you tried deskewing your image before detecting your horizontal and vertical lines? Commented Jul 21, 2020 at 0:04
  • 1
    Try Python Wand, which uses Imagemagick. The have a deskew function based upon the radon transformation. See docs.wand-py.org/en/0.6.1 and imagemagick.org/script/command-line-options.php#deskew. Alternately, do Hough line transform and get the angle of the lines. Then rotate your image. You can also search Google and find a number of links to Python/OpenCV solutions. Commented Jul 21, 2020 at 1:35

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Turns out it was the erosion! I followed your steps and modified it a tad more and it's finally finding all the contours I wanted. The reason I had excessive iterations is because I'm working with several identical images but of varying quality. Some have very poorly defined lines and so it seems I need excessive iterations to fill in gaps. My new problem is that the kernel is dependent on the quality of an image which varies--is there something I can do so that the kernel adjusts based on the quality of an image?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.