Search code examples
pythonopencvimage-processingobject-detectionbinary-image

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


Solution

  • 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)