Search code examples
pythonopencvcontourshape-recognition

How to detect intersecting shapes with OpenCV findContours?


I have two intersecting ellipses in a black and white image. I am trying to use OpenCV findContours to identify the separate shapes as separate contours using this code (and attached image below).

original image

import numpy as np
import matplotlib.pyplot as plt

import cv2
import skimage.morphology

img_3d = cv2.imread("C:/temp/test_annotation_overlap.png")
img_grey = cv2.cvtColor(img_3d, cv2.COLOR_BGR2GRAY)
contours = cv2.findContours(img_grey, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[-2]

fig, ax = plt.subplots(len(contours)+1,1, figsize=(5, 20))

thicker_img_grey = skimage.morphology.dilation(img_grey, skimage.morphology.disk(radius=3))
ax[0].set_title("ORIGINAL IMAGE")
ax[0].imshow(thicker_img_grey, cmap="Greys")

for i, contour in enumerate(contours):
    new_img = np.zeros_like(img_grey)
    cv2.drawContours(new_img, contour, -1,  (255,255,255), 10)
    ax[i+1].set_title(f"Contour {i}")
    ax[i+1].imshow(new_img, cmap="Greys")

plt.show()

However four contours are found, none of which are the original contour:

enter image description here

How can I configure OpenCV.findContours to identify the two separate shapes? (Note I have already played around with Hough circles and found it unreliable for the images I am analysing)


Solution

  • Maybe I overkilled with this approach but it could be used as a working approach. You could find all the contours on the image - you will get the two contours that are like a "semicircle", the contour of the intersection and the contour that is the outer shape of the two addjointed circles. Smallest three contours should be the two semicircles and the intersection. If you draw combinations of two out of these three contours, you will get three mask out of which two will have the combination of one semicircle and the intersection. If you perform closing on the mask you will get your circle. Then you should simply make an algorithm to detect which two masks represent a full circle and you will get your result. Here is the sample solution:

    import numpy as np
    import cv2
    
    
    # Function for returning solidity of contour - ratio of contour area to its 
    # convex hull area.
    def checkSolidity(cnt):
        area = cv2.contourArea(cnt)
        hull = cv2.convexHull(cnt)
        hull_area = cv2.contourArea(hull)
        solidity = float(area)/hull_area
        return solidity
    
    
    img_orig = cv2.imread("circles.png")
    # Had to dilate the image so the contour was completly connected.
    img = cv2.dilate(img_orig, np.ones((3, 3), np.uint8))
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # Grayscale transformation.
    # Otsu threshold.
    thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)[1]
    # Search for contours.
    contours = cv2.findContours(thresh, cv2.CHAIN_APPROX_NONE, cv2.RETR_TREE)[0]
    
    # Sorting contours from smallest to biggest.
    contours.sort(key=lambda cnt: cv2.contourArea(cnt))
    
    # Three contours - two semi circles and the intersection of the circles.
    cnt1 = contours[0]
    cnt2 = contours[1]
    cnt3 = contours[2]
    
    # Create three empty images
    h, w = img.shape[:2]
    mask1 = np.zeros((h, w), np.uint8)
    mask2 = np.zeros((h, w), np.uint8)
    mask3 = np.zeros((h, w), np.uint8)
    
    # Draw all combinations of two out of three contours on the masks.
    # The goal here is to draw one semicircle and the intersection together.
    
    cv2.drawContours(mask1, [cnt1], 0, (255, 255, 255), -1)
    cv2.drawContours(mask1, [cnt3], 0, (255, 255, 255), -1)
    
    cv2.drawContours(mask2, [cnt2], 0, (255, 255, 255), -1)
    cv2.drawContours(mask2, [cnt3], 0, (255, 255, 255), -1)
    
    cv2.drawContours(mask3, [cnt1], 0, (255, 255, 255), -1)
    cv2.drawContours(mask3, [cnt2], 0, (255, 255, 255), -1)
    
    
    # Perform closing operation on the masks so that you get uniform contours.
    kernel_size = 25
    kernel = np.ones((kernel_size, kernel_size), np.uint8)
    mask1 = cv2.morphologyEx(mask1, cv2.MORPH_CLOSE, kernel)
    mask2 = cv2.morphologyEx(mask2, cv2.MORPH_CLOSE, kernel)
    mask3 = cv2.morphologyEx(mask3, cv2.MORPH_CLOSE, kernel)
    
    masks = []  # List for storing all the masks.
    masks.append(mask1)
    masks.append(mask2)
    masks.append(mask3)
    
    # List where you will append solidity of the found biggest contour of every mask.
    solidity = []
    for mask in masks:
        cnts = cv2.findContours(mask, cv2.CHAIN_APPROX_NONE, cv2.RETR_TREE)[0]
        cnt = max(cnts, key=lambda c: cv2.contourArea(c))
        s = checkSolidity(cnt)
        solidity.append(s)
    
    
    # Index of the mask with smallest solidity.
    min_solidity = solidity.index(min(solidity))
    # The mask with the contour that has smallest solidity should be the one that
    # has two semicirles drawn instead of one semicircle and the intersection. 
    #You could build a better function to check which mask is the one with   
    # two semicircles... like maybe the contour with the largest 
    # height and width of the bounding box etc.
    # I chose solidity because it is enough for this example.
    
    # Selection of colors.
    colors = {
        0: (0, 0, 255),
        1: (0, 255, 0),
        2: (255, 0, 0),
    }
    
    # Draw contours of the mask other two masks - those two that have the        
    # semicircle and the intersection.
    for i, s in enumerate(solidity):
        if s != solidity[min_solidity]:
            cnts = cv2.findContours(
                masks[i], cv2.CHAIN_APPROX_NONE, cv2.RETR_TREE)[0]
            cnt = max(cnts, key=lambda c: cv2.contourArea(c))
            cv2.drawContours(img_orig, [cnt], 0, colors[i], 1)
    
    # Display result
    cv2.imshow("img", img_orig)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    Result:

    enter image description here