helpkate1991
helpkate1991

Reputation: 35

Finding each centroid of multiple connected objects

I am SUPER new to python coding and would like some help. I was able to segment each cell outline within a biological tissue (super cool!) and now I am trying to find the centroid of each cell within a tissue using this:

enter image description here

I am using this code:

img = cv2.imread('/Users/kate/Desktop/SegmenterTest/SegmentedCells/Seg1.png')
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY)[1]

cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)

# loop over the contours
for c in cnts:
    # compute the center of the contour
    M = cv2.moments(c)
    cX = int(M["m10"] / M["m00"])
    cY = int(M["m01"] / M["m00"])
    # draw the contour and center of the shape on the image
    cv2.drawContours(image, [c], -1, (0, 255, 0), 2)
    cv2.circle(image, (cX, cY), 7, (255, 255, 255), -1)
    cv2.putText(image, "center", (cX - 20, cY - 20),
        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
    # show the image
    cv2.imshow("Image", image)
    cv2.waitKey(0)

However, when I use this code, it is giving me the centroid of the ENTIRE object, and not each individual object to give this.

enter image description here

I have no idea where to go from here, so a nudge in the right direction would be greatly appreciated!

Upvotes: 1

Views: 2136

Answers (2)

L.Grozinger
L.Grozinger

Reputation: 2398

Problem

cv2.findContours uses an algorithm which has a few different 'retrieval modes'. These affect which contours are returned and how they are returned. This is documented here. These are given as the second argument to findContours. Your code uses cv2.RETR_EXTERNAL which means findContours will only return the outermost border of separate objects.

Solution

Changing this argument to cv2.RETR_LIST will give you all the contours in the image (including the one outermost border). This is the simplest solution.

E.g.

import cv2
import imutils

img = cv2.imread('/Users/kate/Desktop/SegmenterTest/SegmentedCells/Seg1.png')
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY)[1]

cnts = cv2.findContours(thresh.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)

# loop over the contours
for c in cnts:
    # compute the center of the contour
    M = cv2.moments(c)
    cX = int(M["m10"] / M["m00"])
    cY = int(M["m01"] / M["m00"])
    # draw the contour and center of the shape on the image
    cv2.drawContours(image, [c], -1, (0, 255, 0), 2)
    cv2.circle(image, (cX, cY), 7, (255, 255, 255), -1)
    cv2.putText(image, "center", (cX - 20, cY - 20),
        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
    # show the image
    cv2.imshow("Image", image)
    cv2.waitKey(0)

Selecting only the innermost objects

To reliably omit the outer contours you can take advantage of the ability of findContours to return a hierarchy of the contours it detects. To do this, you can change the retrieval mode argument once again to RETR_TREE, which will generate a full hierarchy.

The hierarchy is an array containing arrays of 4 values for each contour in the image. Each value is an index of a contour in the contour array. From the docs:

For each i-th contour contours[i], the elements hierarchy[i][0] , hierarchy[i][1] , hierarchy[i][2], and hierarchy[i][3] are set to 0-based indices in contours of the next and previous contours at the same hierarchical level, the first child contour and the parent contour, respectively. If for the contour i there are no next, previous, parent, or nested contours, the corresponding elements of hierarchy[i] will be negative.

When we say 'innermost', what we mean is contours that have no children (contours inside of them). So we want those contours whose entry in the hierarchy has a negative 3rd value. That is, contours[i], such that hierarchy[i][2] < 0

A small wrinkle is that although findContours returns a tuple which includes the hierarchy, imutils.grabContours discards the hierarchy and returns just the array of contours. All this means is that we have to do the work of grabContours ourselves, if we intend on working with different versions of OpenCV. This is just a simple if else statement.

res = cv2.findContours(thresh.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# switch for different versions of OpenCV
if len(cnts) == 3:
    _, cnts, hierarchy = res
else:
    cnts, hierarchy = res

Once you have hierarchy, checking if a contour, cnts[i] is 'innermost' can be done with hierarchy[0][i][2] < 0, which should be False for contours that contain other contours.

A full example based on your question's code:

import cv2
import imutils

img = cv2.imread('/Users/kate/Desktop/SegmenterTest/SegmentedCells/Seg1.png')
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY)[1]

cnts = cv2.findContours(thresh.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# switch for different versions of OpenCV
if len(cnts) == 3:
    _, cnts, hierarchy = cnts
else:
    cnts, hierarchy = cnts

# loop over the contours
for i, c in enumerate(cnts):
    # check that it is 'innermost'
    if hierarchy[0][i][2] < 0:
        # compute the center of the contour
        M = cv2.moments(c)
        cX = int(M["m10"] / M["m00"])
        cY = int(M["m01"] / M["m00"])

        # draw the contour and center of the shape on the image
        cv2.drawContours(image, [c], -1, (0, 255, 0), 2)
        cv2.circle(image, (cX, cY), 7, (255, 255, 255), -1)
        cv2.putText(image, "center", (cX - 20, cY - 20),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
        # show the image
        cv2.imshow("Image", image)
        cv2.waitKey(0)

Upvotes: 1

Prefect
Prefect

Reputation: 1777

You can use the function regionprops from the module scikit-image in your case. Here is what I got.

Centroids of cells

This is the code I used.

import cv2
import matplotlib.pyplot as plt
from skimage import measure
import numpy as np

cells = cv2.imread('cells.png',0)

ret,thresh = cv2.threshold(cells,20,255,cv2.THRESH_BINARY_INV)


labels= measure.label(thresh, background=0)
bg_label = labels[0,0] 
labels[labels==bg_label] = 0 # Assign background label to 0

props = measure.regionprops(labels)

fig,ax = plt.subplots(1,1)
plt.axis('off')
ax.imshow(cells,cmap='gray')
centroids = np.zeros(shape=(len(np.unique(labels)),2)) # Access the coordinates of centroids
for i,prop in enumerate(props):
    my_centroid = prop.centroid
    centroids[i,:]= my_centroid
    ax.plot(my_centroid[1],my_centroid[0],'r.')

# print(centroids)
# fig.savefig('out.png', bbox_inches='tight', pad_inches=0)
plt.show()

Good luck with your research!

Upvotes: 2

Related Questions