Finding the diameter and area of overlapping ellipses (OpenCV, Python)

Viewed 491

This is my first question on Stackoverflow. I'm a little excited, forgive me if I'm wrong. We have mixed ellipses with and without overlapping drawn randomly from paint. I'm sharing the image I'm working on and my code. I am not a professional in opencv module, I wrote my code as a result of research inspired by sources.

The purpose of my code is,

Detection of randomly drawn with and without overlapping ellipses using the cv2.fitEllipse method. Next, find the major axis, minor axis and areas of the detected ellipses.

The problem with my code is actually this,

In overlapping ellipses, while fitting the ellipse under normal conditions, 2 ellipses should be fit, but about 6-7 ellipses are fit and I cannot reach the values I want to be calculated.

I'm open to your help, thank you in advance.

Example image: With and without overlapping ellipses in image

import cv2
import numpy as np
import random as rng
import math

img = cv2.imread('overlapping_ellipses.png', 1)
imge= cv2.cvtColor(img,cv2.COLOR_RGB2BGR)
gray = cv2.cvtColor(imge, cv2.COLOR_BGR2GRAY)
blur = cv2.blur(gray, (2,2), 3)
edged = cv2.Canny(blur, 50, 100)
kernel= np.ones((2,2))
edged1 = cv2.dilate(edged, kernel, iterations=2)
edged2 = cv2.erode(edged1, kernel, iterations=2)

def thresh_callback(val):
 threshold = val

 canny_output = cv2.Canny(edged2, threshold, threshold * 4)
 contours, _ = cv2.findContours(canny_output, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 minRect = [None]*len(contours)
 minEllipse = [None]*len(contours)
 for i, c in enumerate(contours):
    minRect[i] = cv2.minAreaRect(c)
    if c.shape[0] > 5:
        minEllipse[i] = cv2.fitEllipse(c)
        (x1,y1),(d1,d2),angle = minEllipse[i]
        print('\nX1: ', round(x1,4), '\nY1: ', round(y1,4), '\nD1:',round(d1,4), '\nD2',round(d2,4), '\nAngle:', round(angle,4))
        long= x1-d2
        small= y1-d1
        major= long/2
        minor= small/2
        pixel= 37.795275591
        major1= major/pixel
        minor1= minor/pixel
        print('--------------------------------')
        print('Major axis is: ', abs(round(major1,4)), 'cm')
        print('Minor axis is: ', abs(round(minor1,4)), 'cm')
        print('--------------------------------')
drawing = np.zeros((canny_output.shape[1], canny_output.shape[1], 3), dtype=np.uint8)

for i, c in enumerate(contours):
    color = (rng.randint(0,256), rng.randint(0,256), rng.randint(0,256))
    cv2.drawContours(drawing, contours, i, color)
    if c.shape[0] > 5:
        cv2.ellipse(drawing, minEllipse[i], color, 1)
        
cv2.imshow('Fitting Ellips', drawing)

source_window = 'Source'
cv2.namedWindow(source_window)
cv2.imshow(source_window, img)
max_thresh = 255
thresh = 100
cv2.createTrackbar('Canny Thresh:', source_window, thresh, max_thresh, thresh_callback)
thresh_callback(thresh)
cv2.waitKey()
1 Answers

Step 1: Identify and separate the blobs in the input image.

Since we don't care about colour information here, we can directly load the image as grayscale.

image = cv2.imread('input.png', cv2.IMREAD_GRAYSCALE)

The input image contains black ellipses on white background. We only need the external contours of the blobs, and cv2.findContours expects white blobs on black background. Therefore we need to invert the image. At the same time we need a binary image. We can use cv2.threshold to accomplish both tasks.

Once we detect the blob contours, we can collect some useful information for each blob into a simple map-based data structure.

def detect_blobs(image):
    _,img_binary = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY_INV)
    contours, _ = cv2.findContours(img_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    blobs = []
    for i, contour in enumerate(contours):
        orig_x, orig_y, width, height = cv2.boundingRect(contour)
        roi_image = image[orig_y:orig_y+height,orig_x:orig_x+width]
        blobs.append({
            "i" : i
            , "contour" : contour
            , "origin" : (orig_x, orig_y)
            , "size" : (width, height)
            , "roi_image" : roi_image
        })
    return blobs

Step 2: Process each blob

First we need to determine whether the blob is a single ellipse, or whether it is a pair over intersecting ellipses. One way to do this is by looking for convexity defects.

Since the coordinates of our contour are represented by integers, even the single-ellipse scenario will exhibit some convexity defects. However, their magnitude (the distance between the furthest point on the contour from the enclosing convex hull segment) will be very small, generally below 1 pixel. On the other hand, the contour of a pair of intersecting ellipses will have large convexity defects, one for each of the four points where the curves intersect.

This distinction can be seen on the following two images (contour is blue, convex hull red, identified intersection points/locations of large convexity defects are orange circles):

Single ellipse Two intersecting ellipses

We therefore filter out any small convexity defects, and note the locations of the large ones. Now we're left with 3 possible scenarios.


Scenario A: No intersection points detected

Only small convexity defects were identified, which means this is very likely a single ellipse. We simply fit an ellipse to the contour and move on.

Scenario B: Exactly 4 intersection points detected

In this case we have 2 intersecting ellipses. We use the intersection points to split the contour into 4 segments, one for each "lobe" of the blob. Each of the segments should include the two intersection points that delimit it.

In the following picture, the segments are show in green, yellow, cyan and magenta, while the intersection points are orange circles:

Now, we can combine the pairs of segments that lie opposite each other (i.e. green+cyan and yellow+magenta) to get two lists of points, one for each ellipse. Again, we simply fit an ellipse to each list of points.

Scenario C: Some other number of intersection points detected

This is considered an invalid situation.

def process_blob(blob):
    MAJOR_DEFECT_THRESHOLD = 2.0
    
    contour = blob["contour"]
    blob["hull"] = cv2.convexHull(contour)
    
    hull_idx = cv2.convexHull(contour, returnPoints=False)
    defects = cv2.convexityDefects(contour, hull_idx)
    
    intersections = []
    for i,defect in enumerate(np.squeeze(defects, 1)):
        _, _, far_idx, far_dist = defect
        real_far_dist = far_dist / 256.0
        if real_far_dist >= MAJOR_DEFECT_THRESHOLD:
            intersections.append(far_idx)
    
    if len(intersections) == 0:
        print("One ellipse")
        blob["ellipses"] = [cv2.fitEllipse(contour)]
    elif len(intersections) == 4:
        print("Two ellipses")
        blob["segments"] = [
            contour[intersections[0]:intersections[1]+1]
            , contour[intersections[1]:intersections[2]+1]
            , contour[intersections[2]:intersections[3]+1]
            , np.vstack([contour[intersections[3]:],contour[:intersections[0]+1]])
        ]
        split_contours = [
            np.vstack([blob["segments"][0], blob["segments"][2]])
            , np.vstack([blob["segments"][1], blob["segments"][3]])
        ]
        blob["ellipses"] = [cv2.fitEllipse(c) for c in split_contours]
    else:
        print("Invalid scenario")
        blob["ellipses"] = []
        
    return blob["ellipses"]

At this point, it's trivial to calculate the parameters you need -- I'll leave this as an excercise to the reader.

As a bonus, here's some simple visualization for debugging purposes:

def visualize_blob(blob):
    PADDING = 20
    
    orig_x, orig_y = blob["origin"]
    offset = (orig_x - PADDING, orig_y - PADDING)
    
    input_img = cv2.copyMakeBorder(blob["roi_image"]
        , PADDING, PADDING, PADDING, PADDING
        , cv2.BORDER_CONSTANT, None, 255)

    adjusted_img = cv2.add(input_img, 127) - 63
    output_img_ch = cv2.cvtColor(adjusted_img, cv2.COLOR_GRAY2BGR)
    output_img_seg = output_img_ch.copy()
    output_img_el = output_img_ch.copy()
    
    cv2.drawContours(output_img_ch, [blob["hull"] - offset], 0, (127,127,255), 4)
    cv2.drawContours(output_img_ch, [blob["contour"] - offset], 0, (255,127,127), 2)
    
    SEGMENT_COLORS = [(0,255,0),(0,255,255),(255,255,0),(255,0,255)]
    if "segments" in blob:
        for i in range(4):
            cv2.polylines(output_img_seg, [blob["segments"][i] - offset], False, SEGMENT_COLORS[i], 4)
        for i in range(4):
            center = (blob["segments"][i] - offset)[0][0]
            cv2.circle(output_img_ch, center, 4, (0,191,255), -1)
            cv2.circle(output_img_seg, center, 4, (0,191,255), -1)
        
    
    for ellipse in blob["ellipses"]:
        offset_ellipse = ((ellipse[0][0] - offset[0], ellipse[0][1] - offset[1]), ellipse[1], ellipse[2])
        cv2.ellipse(output_img_el, offset_ellipse, (0,0,255), 2)
    
    cv2.imshow('', np.hstack([output_img_ch,output_img_seg, output_img_el]))
    cv2.imwrite('output_%d_ch.png' % blob["i"], output_img_ch)
    cv2.imwrite('output_%d_seg.png' % blob["i"], output_img_seg)
    cv2.imwrite('output_%d_el.png' % blob["i"], output_img_el)
    cv2.waitKey()

Pulling it all together:

import cv2
import numpy as np

## INSERT THE FUNCTIONS LISTED ABOVE IN THE QUESTION ##

image = cv2.imread('input.png', cv2.IMREAD_GRAYSCALE)

blobs = detect_blobs(image)
print("Found %d blob(s)." % len(blobs))

for blob in blobs:
    process_blob(blob)
    visualize_blob(blob)
Related