Puki Duck
Puki Duck

Reputation: 3

A binary image has a black shape with white spots and white background with black spots. How to detect the square OpenCV python

I'm trying to detect a polygonal shape in a binary image. If the shape is white then the background is black and vice versa. For example if the shape is white it has some black spots in it and the background is black and has some white spots in it. If i'm using OpenCV's tools for object detection (canny, contours, lines, etc...) it works badly and detects things that are not the shape, or rare occasions it does detect the shape but does so badly.

Example of an image with a shape

I tried using canny edge detection and contours and many things with lines but none of the methods worked. I did not provide the results because i didn't save them and i tried many things and they were not even in the direction of detecting it correctly.

What im expecting

Upvotes: 0

Views: 725

Answers (1)

Rotem
Rotem

Reputation: 32094

Getting a perfect solution is challenging.
We may find an approximated solution using the following stages:

Start by closing and opening morphological operations.
Apply closing with 5x5 kernel, followed by opening with 3x3 kernel - connecting neighbors without too much dilating:

mask = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))

Result:
enter image description here


Removing small contours:

contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

for i, c in enumerate(contours):
    area_tresh = 1000
    area = cv2.contourArea(c)
    if area < area_tresh:  # Fill small contours with black color
         cv2.drawContours(mask, contours, i, 0, cv2.FILLED)

Result:
enter image description here


Apply opening in the vertical direction and then in the horizontal direction:

mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((51, 1), np.uint8))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((1, 51), np.uint8))

Result:
enter image description here


Find largest "black" contour, and approximate it to a rectangle using simplify_contour:

contours, hierarchy = cv2.findContours(255 - mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
c = max(contours, key=cv2.contourArea)  

# Simplify contour to rectangle
approx = simplify_contour(c, 4)

out_img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)  # Convert to BGR before drawing colored contour.

cv2.drawContours(out_img, [approx], 0, (0, 255, 0), 3)

Result:
enter image description here


For finding the best inner rectangle we my use one of the solutions from the following post.


Code sample:

import cv2
import numpy as np

# https://stackoverflow.com/a/55339684/4926757
def simplify_contour(contour, n_corners=4):
    """
    Binary searches best `epsilon` value to force contour 
    approximation contain exactly `n_corners` points. 
    :param contour: OpenCV2 contour.
    :param n_corners: Number of corners (points) the contour must contain.
    :returns: Simplified contour in successful case. Otherwise returns initial contour.
    """
    n_iter, max_iter = 0, 100
    lb, ub = 0.0, 1.0

    while True:
        n_iter += 1
        if n_iter > max_iter:
            return contour

        k = (lb + ub)/2.
        eps = k*cv2.arcLength(contour, True)
        approx = cv2.approxPolyDP(contour, eps, True)

        if len(approx) > n_corners:
            lb = (lb + ub)/2.0
        elif len(approx) < n_corners:
            ub = (lb + ub)/2.0
        else:
            return approx


img = cv2.imread('white_spots.png', cv2.IMREAD_GRAYSCALE)  # Read input image as grayscale (assume binary image).
#thresh = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)[1]  # Convert to binary image - use automatic thresholding and invert polarity
thresh = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY)[1]  # Convert to binary image (not really required).

# Apply closing with 5x5 kernel, followed by opening with 3x3 kernel - connecting neighbors without too much dilating.
mask = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))

cv2.imwrite('mask1.png', mask)  # Save for testing

# Find contours, (use cv2.RETR_EXTERNAL)
contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

# Remove small contours
for i, c in enumerate(contours):
    area_tresh = 1000
    area = cv2.contourArea(c)
    if area < area_tresh:  # Fill small contours with black color
         cv2.drawContours(mask, contours, i, 0, cv2.FILLED)

cv2.imwrite('mask2.png', mask)  # Save for testing

# Apply opening in the vertical direction and then in the horizontal direction
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((51, 1), np.uint8))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((1, 51), np.uint8))

cv2.imwrite('mask3.png', mask)  # Save for testing

# Find largest "black" contour.
contours, hierarchy = cv2.findContours(255 - mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
c = max(contours, key=cv2.contourArea)  

# Simplify contour to rectangle
approx = simplify_contour(c, 4)

out_img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)  # Convert to BGR before drawing colored contour.

cv2.drawContours(out_img, [approx], 0, (0, 255, 0), 3)

cv2.imwrite('out_img.png', out_img)

Upvotes: 1

Related Questions