pythonimageopencvimage-processingimage-morphology

OpenCV counting overlapping circles using morphological operation


Here is an image which I am trying to get the circles from.

enter image description here

I used difference of gray image and erosion to get the boundaries.

img_path= 'input_data/coins.jpg'
img = cv2.imread(img_path)
rgb,gray=getColorSpaces(img)
a,b=0,255
plt.figure(figsize=(12, 12))

erosion_se=cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
erosion = cv2.erode(gray,erosion_se,iterations = 1)
boundary=gray-erosion
image, contours, hierarchy = cv2.findContours(boundary,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)    
plt.imshow(boundary,'gray')

I could get the circles for most of the circles whose boundaries are relatively distinct. I want to do two things

    circles = cv2.HoughCircles(boundary, cv2.HOUGH_GRADIENT, 1, 20,
                  param1=30,
                  param2=15,
                  minRadius=5,
                  maxRadius=20)

    if circles is not None: 
        circles = np.uint16(np.around(circles))
        for i in circles[0,:]:
            cv2.circle(img,(i[0],i[2]),i[3],(0,255,0),2)
            cv2.circle(img,(i[0],i[2]),2,(0,0,255),3)

        cv2.imshow('circles', img)

        k = cv2.waitKey(0)
        if k == 27:
            cv2.destroyAllWindows()

Below is the output after HoughCircles from the circles boundary image.The big green circle which stands out is undesired.I am not sure why for some of the overlapping regions ,the circles are not detected.


Solution

  • Instead of using HoughCircles which requires the circles to be "perfect" circles and is inaccurate on connected blobs, a simple contour filtering approach should work. Here's the main idea:

    To count the number of overlapping circles

    To find circles touching the image boundary, we limit the detection area to only the outer 10 pixels on the image. We find contours on this new image and then filter using contour area to determine touching circles


    Count number of overlapping circles

    After converting to grayscale and thresholding to obtain a binary image, we approximate the contour area of a single blob/circle as ~375. Next we find contours on the image and filter using cv2.contourArea(). To determine if there is overlap, we divide the area of each contour by the single circle area then find the ceiling using math.ceil(). If we get a ceiling value greater than 1, it means that the blob was connected and we simply add the ceiling value to our counter

    Here's the detected overlapping circles

    Overlapping: 213

    Find circles touching image boundary

    The idea is to create a black box to mask out the inner part of the image that is not on the boundary. We can do this with cv2.fillPoly(). From here we find contours and filter using contour area. The idea is that if the blob is relatively big compared to some threshold area, it means that the blob is most likely touching the edge

    Here's the filled in black box and the detected touching circles

    Touching: 10

    import cv2
    import numpy as np
    import math
    
    image = cv2.imread('1.jpg')
    black_box = image.copy()
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
    
    # Count overlapping circles
    single_area = 375
    overlapping = 0
    
    cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = cnts[0] if len(cnts) == 2 else cnts[1]
    for c in cnts:
        area = cv2.contourArea(c)
        blob_area = math.ceil(area/single_area)
        if blob_area > 1:
            overlapping += blob_area
            cv2.drawContours(image, [c], -1, (36,255,12), 2)
    
    # Find circles touching image boundary
    h, w, _ = image.shape
    boundary = 10
    touching = 0
    box = np.array(([boundary,boundary], 
                          [w-boundary,boundary], 
                          [w-boundary, h-boundary], 
                          [boundary, h-boundary]))
    cv2.fillPoly(black_box, [box], [0,0,0])
    
    copy = black_box.copy()
    copy = cv2.cvtColor(copy, cv2.COLOR_BGR2GRAY)
    copy = cv2.threshold(copy, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
    
    cnts = cv2.findContours(copy, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = cnts[0] if len(cnts) == 2 else cnts[1]
    for c in cnts:
        area = cv2.contourArea(c)
        if area > 100:
            touching += 1
            cv2.drawContours(black_box, [c], -1, (36,255,12), 2)
    
    print('Overlapping:', overlapping)
    print('Touching:', touching)
    cv2.imshow('image', image)
    cv2.imshow('black_box', black_box)
    cv2.waitKey()