Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to find the centre of these sometimes-overlapping circles

As part of a project I'm working on, I need to find the centre-point of some "blobs" in an image using OpenCV with Python. I'm having a bit of trouble with it, and would truly appreciate any help or insight :)

My current method is to: get the contours of the images, overlay ellipses on those, use the blob detector to find the centre of each of these. This works fairly well, but occasionally I have extraneous blobs that I need to ignore, and sometimes the blobs are touching each-other.

Here's an example of when it goes well: Good source image: Good source image After extracting contours: After extracting contours With the blobs detected: With the blobs detected

And when it goes poorly (you can see that it's incorrectly overlayed an ellipse over three blobs, and detected one that I don't want): Bad source image: Bad source image After extracting contours: After extracting contours With the blobs detected: With the blobs detected

This is the code I currently use. I'm unsure of any other option.

def process_and_detect(img_path):
    img = cv2.imread(path)
    imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    ret, thresh = cv2.threshold(imgray, 50, 150, 0)
    im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    drawn_img = np.zeros(img.shape, np.uint8)
    min_area = 50
    min_ellipses = []
    for cnt in contours:
        if cv2.contourArea(cnt) >= min_area:
            ellipse = cv2.fitEllipse(cnt)
            cv2.ellipse(drawn_img,ellipse,(0,255,0),-1)
    plot_img(drawn_img, size=12)

    # Change thresholds
    params = cv2.SimpleBlobDetector_Params()
    params.filterByColor = True
    params.blobColor = 255
    params.filterByCircularity = True
    params.minCircularity = 0.75
    params.filterByArea = True
    params.minArea = 150
    # Set up the detector
    detector = cv2.SimpleBlobDetector_create(params)

    # Detect blobs.
    keypoints = detector.detect(drawn_img)
    for k in keypoints:
        x = round(k.pt[0])
        y = round(k.pt[1])
        line_length = 20
        cv2.line(img, (x-line_length, y), (x+line_length, y), (255, 0, 0), 2)
        cv2.line(img, (x, y-line_length), (x, y+line_length), (255, 0, 0), 2)
    plot_img(img, size=12)

Thank you so much for reading this far, I sincerely hope someone can help me out, or point me in the right direction. Thanks!

like image 680
Ash Hall Avatar asked Dec 24 '22 15:12

Ash Hall


1 Answers

Blob detector

Currently, your implementation is redundant. From the SimpleBlobDetector() docs:

The class implements a simple algorithm for extracting blobs from an image:

  1. Convert the source image to binary images by applying thresholding with several thresholds from minThreshold (inclusive) to maxThreshold (exclusive) with distance thresholdStep between neighboring thresholds.
  2. Extract connected components from every binary image by findContours() and calculate their centers.
  3. Group centers from several binary images by their coordinates. Close centers form one group that corresponds to one blob, which is controlled by the minDistBetweenBlobs parameter.
  4. From the groups, estimate final centers of blobs and their radiuses and return as locations and sizes of keypoints.

So you're implementing part of the steps already, which might give some unexpected behavior. You could try playing with the parameters to see if you can figure out some that work for you (try creating trackbars to play with the parameters and get live results of your algorithm with different blob detector parameters).

Modifying your pipeline

However, you've already got most of your own pipeline written, so you can easily remove the blob detector and implement your own algorithm. If you simply drop your threshold a bit, you can easily get clearly marked circles, and then blob detection is as simple as contour detection. If you have a separate contour for each blob, then you can calculate the centroid of the contour with moments(). For example:

def process_and_detect(img_path):

    img = cv2.imread(img_path)
    imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    ret, thresh = cv2.threshold(imgray, 100, 255, cv2.THRESH_BINARY)

    contours = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[1]
    line_length = 20
    for c in contours:
        if cv2.contourArea(c) >= min_area:
            M = cv2.moments(c)
            x = int(M['m10']/M['m00'])
            y = int(M['m01']/M['m00']) 
            cv2.line(img, (x-line_length, y), (x+line_length, y), (255, 0, 0), 2)
            cv2.line(img, (x, y-line_length), (x, y+line_length), (255, 0, 0), 2)

Detected centroids

Getting more involved

This same pipeline can be used to automatically loop through threshold values so you don't have to guess and hardcode those values. Since the blobs all seem roughly the same size, you can loop through until all contours have roughly the same area. You could do this for e.g. by finding the median contour size, defining some percentage of that median size above and below that you'll allow, and checking if all the contours detected fit in those bounds.

Here's an animated gif of what I mean. Notice that the gif stops once the contours are separated:

Contour areas shrinking until similar

Then you can simply find the centroids of those separated contours. Here's the code:

def process_and_detect(img_path):

    img = cv2.imread(img_path)
    imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    for thresh_val in range(0, 255):

        # threshold and detect contours
        thresh = cv2.threshold(imgray, thresh_val, 255, cv2.THRESH_BINARY)[1]
        contours = cv2.findContours(thresh,
                                    cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[1]

        # filter contours by area
        min_area = 50
        filtered_contours = [c for c in contours
                             if cv2.contourArea(c) >= min_area]
        area_contours = [cv2.contourArea(c) for c in filtered_contours]

        # acceptable deviation from median contour area
        median_area = np.median(area_contours)
        dev = 0.3
        lowerb = median_area - dev*median_area
        upperb = median_area + dev*median_area

        # break when all contours are within deviation from median area
        if ((area_contours > lowerb) & (area_contours < upperb)).all():
            break

    # draw center location of blobs
    line_length = 8
    cross_color = (255, 0, 0)
    for c in filtered_contours:
        M = cv2.moments(c)
        x = int(M['m10']/M['m00'])
        y = int(M['m01']/M['m00'])
        cv2.line(img, (x-line_length, y), (x+line_length, y), cross_color, 2)
        cv2.line(img, (x, y-line_length), (x, y+line_length), cross_color, 2)

Centroids found

Note that here I looped through all possible threshold values with range(0, 255) to give 0, 1, ..., 254 but really you could start higher and skip through a few values at a time with, say, range(50, 200, 5) to get 50, 55, ..., 195 which would of course be much faster.

like image 154
alkasm Avatar answered Mar 02 '23 14:03

alkasm