简体   繁体   中英

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

原始图像

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:

在此处输入图像描述

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)

Philosophically, you want to find two circles , because you search for them, you expect centers and radii. Graphically the figures are connected, we can see them separated because we know what a "circle" is and extrapolate the coordinates, which match the parts which overlap.

So what about finding the minimum enclosing circle for each contour (or in some cases fitEllipse and use the their parameters): https://docs.opencv.org/master/dd/d49/tutorial_py_contour_features.html

Then say draw that circle in a clear image and take the pixel coordinates which are not zero - by a mask or compute them by drawing a circle step by step.

Then compare these coordinates with the coordinates in the other contours with some precision and append the matching coordinates to the current contour.

Finally: draw the extended contour on a clear canvas and apply HoughCircles for a single non-overlapping circle. (Or compute the center and radius, the coordinates of a circle and comparing to the contour with a precision.)

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:

在此处输入图像描述

For reference here I will post the solution I came up with based on some ideas here and a few more. This solution was 99.9% effective and recovering ellipses from images often including many overlapping, contained, and with other image noise such as lines, text and so on.

The code is too length and distributed to post here but the pseudocode is as follows.

  1. Segment the image
    • Run cv2 findContours with RETR_EXTERNAL to get separate regions in the image
    • For each image, fill in the interior, apply mask, and extract the region to be processed independently of other regions.
    • Remaining steps are executed for each region independently
  2. Run cv2 findContours with RETR_LIST to get all internal and external contours
  3. For each contour found, apply polygon smoothing to reduce effect of pixellation
  4. For each smoothed contour, identify the continuous segments within that contour which have the same curvature sign ie segments which are entirely curving right, or curving left (just compute angles and sign changes)
  5. Within each segment, fit an ellipse model with least squares (scikit-learn EllipseModel)
  6. Perform the Lee algorithm on the original image to compute for each pixel its minimum distance to a white pixel
  7. For each model, perform a greedy local neighbourhood search to improve the fit against the original - the fit being the fitted ellipse maximum distance to white pixel, from the output of the lee algorithm

Not simple or elegant but is highly accurate for the content I am dealing with confirmed from manual review of a large number of images.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM