Identifying imperfect shapes with noisy backgrounds with OpenCV

Viewed 829

I am trying to identify a rectangle underwater in a noisy environment. I implemented Canny to find the edges, and drew the found edges using cv2.circle. From here, I am trying to identify the imperfect rectangle in the image (the black one below the long rectangle that covers the top of the frame)

rectangle

I have attempted multiple solutions, including thresholds, blurs and resizing the image to detect the rectangle. Below is the barebones code with just drawing the identified edges.

import numpy as np
import cv2
import imutils

img_text = 'img5.png'
img = cv2.imread(img_text)
original = img.copy()

min_value = 50
max_value = 100

# draw image and return coordinates of drawn pixels
image = cv2.Canny(img, min_value, max_value)
indices = np.where(image != 0)
coordinates = zip(indices[1], indices[0])

for point in coordinates:
    cv2.circle(original, point, 1, (0, 0, 255), -1)

cv2.imshow('original', original)
cv2.waitKey(0)
cv2.destroyAllWindows()

Where the output displays this:

output

From here I want to be able to separately detect just the rectangle and draw another rectangle on top of the output in green, but I haven't been able to find a way to detect the original rectangle on its own.

1 Answers

For your specific image, I obtained quite good results with a simple thresholding on the blue channel.

image = cv2.imread("test.png")
t, img = cv2.threshold(image[:,:,0], 80, 255, cv2.THRESH_BINARY)

enter image description here


In order to adapt the threshold, I propose a simple way of varying the threshold until you get one component. I have also implemented the rectangle drawing:

def find_square(image):

    markers = 0
    threshold = 10

    while np.amax(markers) == 0:
        threshold += 5
        t, img = cv2.threshold(image[:,:,0], threshold, 255, cv2.THRESH_BINARY_INV)
        _, markers = cv2.connectedComponents(img)

    kernel = np.ones((5,5),np.uint8)

    img = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
    img = cv2.morphologyEx(img, cv2.MORPH_DILATE, kernel)

    nonzero = cv2.findNonZero(img)
    x, y, w, h = cv2.boundingRect(nonzero)
    cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), 2)

    cv2.imshow("image", image)

And the results on the provided example images:

enter image description here enter image description here enter image description here

The idea behind this approach is based on the observation that the most information is in the blue channel. If you separate the images in the channels, you will see that in the blue channel, the dark square has the best contrast. It is also the darkest region on this channel, which is why thresholding works. The problem remains the threshold setting. Based on the above intuition, we are looking for the lowest threshold that will bring up something (and hope that it will be the square). What I did is to simply increase gradually the threshold until something appears.

Then, I applied some morphology operations to eliminate other small points that may appear after thresholding and to make the square look a bit bigger (the edges of the square are lighter, and therefore not the entire square is captured). Then is was a matter of drawing the rectangle.

The code can be made much nicer (and more efficient) by doing some statistical analysis on the histogram. Simply compute the threshold such that 5% (or some percent) of the pixels are darker. You may require do so a connected component analysis to keep the biggest blob.

Also, my usage of connectedComponents is very poor and inefficient. Again, code written in a hurry to prove the concept.

Related