Python opencv detect shapes with intersections

Viewed 1455

We are conducting shape detection using Python openCV. We find approx_poly_dp and then count number of vertices. However, the process does not work when object has many intersections (seen in second picture below). Opencv cannot detect individual objects, and just finds one large shape. In dealing with intersections, what is best way to find shapes, given general picture inputs. Or is this not a functionality of opencv, maybe better suited with machine learning?

    image_original = cv2.imread(file_name)
    image_gray = cv2.cvtColor(image_original, cv2.COLOR_BGR2GRAY)
    image_blur = cv2.GaussianBlur(image_gray, (5, 5), 0)
    image_morph = cv2.morphologyEx(image_blur, cv2.MORPH_OPEN, kernel)
    image_canny = cv2.Canny(image_morph, 50, 50)
    image_dilate = cv2.dilate(image_canny, kernel, iterations=2)
    image_final = cv2.erode(image_dilate, kernel, iterations=1)

    contours, hierarchy = cv2.findContours(image_final, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    peri = cv2.arcLength(contours, True)
    approx = cv2.approxPolyDP(contours, 0.04 * peri, True)

    if len(approx) == 3:
        shape = "triangle"
    elif len(approx) == 4:
        (x, y, w, h) = cv2.boundingRect(approx)
        ar = w / float(h)
        shape = "square" if ar >= 0.95 and ar <= 1.05 else "rectangle"
    elif len(approx) == 5:
        shape = "pentagon"
    else:
        shape = "circle"

enter image description here

Intersection Picture:

enter image description here

2 Answers

Following procedure might work for simple figures:

Find the connected components, and find neighboring components of each component ignoring the background and outline components.

Try a component with combinations of its neighboring components and feed it to the shape classifier that you already have. As a pre-processing step, you might have to combine the components into a single blob using a morphological closing.

import cv2 as cv
import numpy as np
from itertools import combinations

im = np.zeros((512, 512), dtype=np.uint8)
im = cv.rectangle(im, (100, 200), (400, 400), 255, 2)
im = cv.circle(im, (250, 380), 100, 255, 2)
im = cv.rectangle(im, (50, 50), (250, 250), 255, 2)

ret, bw = cv.threshold(im, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU)
ncomp, labels, stats, centroids = cv.connectedComponentsWithStats(bw)

def getNeighbors(labels, n):
    r = 4 # you might have to change this depending on outline thickness
    se = cv.getStructuringElement(cv.MORPH_CROSS, (2*r+1, 2*r+1))
    neighbors = cv.dilate((labels==n).astype(np.uint8), se)
    # ignore background, outline and the component of interest
    return np.setdiff1d(np.unique(labels[neighbors==1]), [0, 1, n])

def shapes(arr, i):
    p = arr.tolist()
    for r in range(1, len(p)+1):
        for l in list(combinations(p, r)):
            print l + (i, )

For the following figure, you get the listed combinations of connected components. Note that I haven't bothered to remove duplicates.

for i in range(2, ncomp):
    shapes(getNeighbors(labels, i), i)

(3, 2)
(2, 3)
(4, 3)
(2, 4, 3)
(3, 4)
(5, 4)
(3, 5, 4)
(4, 5)
(6, 5)
(4, 6, 5)
(5, 6)

im1 cc

I wouldn't say this is an efficient solution as it involves trying out all combinations of neighbors, but I'm publishing this just in case someone finds this image processing based solution useful.

I came up with a solution to detect shape from an intersected image by using parent and child contour relation and contour smoothing method.

Output image one

In the above last image red border is the parent contour and yellow is the child contour's border. Iterate all child contour with parent contour apply contour smoothing approach to extract shapes. This error smoothing approach is discussed in my question by ian-chu

import cv2
import numpy as np


def get_parent_contour(contours, hierarchy):
    parent = hierarchy[0, :, 3]
    unique_parent, counts_parent = np.unique(parent, return_counts=True)

    parent_contour_id = -1
    max_area = 0

    for c in unique_parent:
        if c != -1:
            area = cv2.contourArea(contours[c])
            if area > max_area:
                max_area = area
                parent_contour_id = c

    return parent_contour_id, contours[parent_contour_id]


def get_child_contours(contours, hierarchy, parent_contour_id):
    parent = hierarchy[0, :, 3]

    # Select all contour whose parent is selected parent above
    child_contour_id = [i for i, v in enumerate(parent) if v == parent_contour_id]
    child_contours = [contours[i] for i in child_contour_id]

    return child_contour_id, child_contours


def smoothed(contour, dists, cutoff):
    smooth_con = []
    for a in range(len(dists)):
        if dists[a] < cutoff:
            smooth_con.append(contour[a])
    return np.asarray(smooth_con)


# get the distance list for an array of points
def distList(src, other):
    dists = []
    for point in src:
        point = point[0]  # drop extra brackets
        _, dist = closestPoint(point, other)
        dists.append(dist)
    return dists


# returns squared distance of two points
def squaredDist(one, two):
    dx = one[0] - two[0]
    dy = one[1] - two[1]
    return dx * dx + dy * dy


# find closest point (just do a linear search)
def closestPoint(point, arr):
    # init tracker vars
    closest = None
    best_dist = np.Inf

    # linear search
    for other in arr:
        other = other[0]  # remove extra brackets
        dist = squaredDist(point, other)
        if dist < best_dist:
            closest = other
            best_dist = dist
    return closest, best_dist


image = cv2.imread("image.png")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

_, threshold = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV)
cv2.imshow("threshold", threshold)

contours, hierarchy = cv2.findContours(threshold, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

blank_image = np.zeros_like(image)

if hierarchy is not None:

    for contour in contours:
        color = np.random.randint(0, 255, size=(3,))
        color = (int(color[0]), int(color[1]), int(color[2]))
        cv2.drawContours(blank_image, [contour], 0, color, 2)

cv2.imshow("contour image", blank_image)

parent_contour_id, parent_contour = get_parent_contour(contours, hierarchy)
child_contour_ids, child_contours = get_child_contours(contours, hierarchy, parent_contour_id)

blank_image_2 = np.zeros_like(image)
cv2.drawContours(blank_image_2, [parent_contour], 0, (0, 0, 255), 2)

for child_cnt in child_contours:
    cv2.drawContours(blank_image_2, [child_cnt], 0, (0, 255, 255), 1)
cv2.imshow("parent child contours", blank_image_2)

for child_cnt in child_contours:

    one = parent_contour
    two = child_cnt

    # get distances
    one_dists = distList(one, two)
    two_dists = distList(two, one)
    try:
        # dump values greater than 10
        smooth_one = smoothed(one, one_dists, 35)
        smooth_two = smoothed(two, two_dists, 35)

        # draw new contour
        blank_image_3 = np.zeros_like(threshold)
        cv2.drawContours(blank_image_3, [smooth_one], -1, 255, -1)
        cv2.drawContours(blank_image_3, [smooth_two], -1, 0, -1)

        cv2.imshow("smooth image", blank_image_3)
        
        # you can apply filters on smoothed contour image to accepting and reject smooth contour
        # And apply single shape detection algorithm on smoothed contour image to find shapes
        
    except:
        print("no near points. ")

    cv2.waitKey(0)

cv2.destroyAllWindows()

Smooth output Images

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

Related