You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何提取检测凹点并拟合椭圆?触摸细胞分割技术求助

Extracting Concave Points & Fitting Ellipses for Touching Cell Segmentation

Got it, let's break down how to solve your problem—you've already got contours and convex hulls working, now we need to dig into concave points and use them to split and fit ellipses for touching cells.

Step 1: Extract Concave Points Using Convexity Defects

OpenCV has a built-in function cv2.convexityDefects() that identifies gaps between a contour and its convex hull—these gaps correspond to the concave regions where cells touch. Here's how to integrate this into your existing code:

First, modify your convex hull calculation to return indices (instead of raw points) since convexity defects rely on index mapping:

import numpy as np
import cv2

img = cv2.imread("binary_img.png")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh_img = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
thresh_img = 255 - thresh_img
contours, hierarchy = cv2.findContours(thresh_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# Calculate convex hulls with indices (needed for convexity defects)
hulls = []
hull_indices = []
for cnt in contours:
    if len(cnt) > 3:
        hull = cv2.convexHull(cnt)
        hull_idx = cv2.convexHull(cnt, returnPoints=False)
        hulls.append(hull)
        hull_indices.append(hull_idx)
    else:
        hulls.append(None)
        hull_indices.append(None)

# Calculate convexity defects
convex_defects = []
for i in range(len(contours)):
    cnt = contours[i]
    idx = hull_indices[i]
    if cnt is not None and idx is not None and len(idx) > 3:
        defects = cv2.convexityDefects(cnt, idx)
        convex_defects.append(defects)
    else:
        convex_defects.append(None)

Next, filter these defects to get meaningful concave points (ignore small gaps that are just noise):

# Extract valid concave points (adjust distance threshold based on your image)
concave_points_list = []
distance_threshold = 20  # Tweak this! Units are square pixels, so we'll take sqrt later

for i in range(len(convex_defects)):
    defects = convex_defects[i]
    concave_points = []
    if defects is not None:
        for j in range(defects.shape[0]):
            s, e, f, d = defects[j, 0]
            # Convert distance from squared pixels to actual pixel distance
            dist = np.sqrt(d)
            if dist > distance_threshold:
                # Get the farthest point in the defect (this is our concave point)
                farthest_pt = tuple(contours[i][f][0])
                concave_points.append(farthest_pt)
    concave_points_list.append(concave_points)

You can visualize these concave points by adding this to your drawing code:

# Draw contours, hulls, and concave points
drawing = np.zeros((thresh_img.shape[0], thresh_img.shape[1], 3), np.uint8)
for i in range(len(contours)):
    cnt = contours[i]
    hull = hulls[i]
    concave_pts = concave_points_list[i]
    
    # Draw contour (green)
    cv2.drawContours(drawing, [cnt], 0, (0, 255, 0), 1, 8, hierarchy)
    # Draw convex hull (blue)
    if hull is not None:
        cv2.drawContours(drawing, [hull], 0, (255, 0, 0), 1, 8)
    # Draw concave points (red circles)
    for pt in concave_pts:
        cv2.circle(drawing, pt, 3, (0, 0, 255), -1)

cv2.imwrite("concave_points.png", drawing)
cv2.imshow("Contours + Hulls + Concave Points", drawing)
cv2.waitKey(0)

Step 2: Split Touching Contours & Fit Ellipses

Now that you have concave points, you can use them to split merged cell contours into individual ones, then fit ellipses to each segment. For simple two-cell touches, here's a basic implementation:

# Create a copy of the drawing to draw ellipses
ellipse_drawing = drawing.copy()

for i in range(len(contours)):
    cnt = contours[i]
    concave_pts = concave_points_list[i]
    
    # Skip if contour is too small to fit an ellipse (needs at least 5 points)
    if len(cnt) < 5:
        continue
    
    if len(concave_pts) >= 2:
        # Find the two farthest concave points (likely the split line between two cells)
        max_dist = 0
        split_pt1 = None
        split_pt2 = None
        for p1_idx in range(len(concave_pts)):
            for p2_idx in range(p1_idx + 1, len(concave_pts)):
                p1 = np.array(concave_pts[p1_idx])
                p2 = np.array(concave_pts[p2_idx])
                dist = np.linalg.norm(p1 - p2)
                if dist > max_dist:
                    max_dist = dist
                    split_pt1 = concave_pts[p1_idx]
                    split_pt2 = concave_pts[p2_idx]
        
        # Find the indices of these split points in the original contour
        def find_contour_index(contour, target_pt):
            min_dist = float('inf')
            idx = 0
            for k in range(len(contour)):
                pt = tuple(contour[k][0])
                dist = np.linalg.norm(np.array(pt) - np.array(target_pt))
                if dist < min_dist:
                    min_dist = dist
                    idx = k
            return idx
        
        idx1 = find_contour_index(cnt, split_pt1)
        idx2 = find_contour_index(cnt, split_pt2)
        
        # Split the contour into two sub-contours
        if idx1 < idx2:
            sub_cnt1 = cnt[idx1:idx2+1]
            sub_cnt2 = np.vstack([cnt[idx2:], cnt[:idx1+1]])
        else:
            sub_cnt1 = cnt[idx2:idx1+1]
            sub_cnt2 = np.vstack([cnt[idx1:], cnt[:idx2+1]])
        
        # Fit ellipses to each sub-contour
        if len(sub_cnt1) >=5:
            ellipse1 = cv2.fitEllipse(sub_cnt1)
            cv2.ellipse(ellipse_drawing, ellipse1, (0, 255, 255), 2)
        if len(sub_cnt2) >=5:
            ellipse2 = cv2.fitEllipse(sub_cnt2)
            cv2.ellipse(ellipse_drawing, ellipse2, (0, 255, 255), 2)
    else:
        # No concave points = single cell, fit ellipse directly
        ellipse = cv2.fitEllipse(cnt)
        cv2.ellipse(ellipse_drawing, ellipse, (0, 255, 255), 2)

cv2.imwrite("ellipse_segmentation.png", ellipse_drawing)
cv2.imshow("Ellipse Segmentation", ellipse_drawing)
cv2.waitKey(0)
cv2.destroyAllWindows()

Key Notes to Tweak for Your Data

  • Threshold Adjustment: The distance_threshold for convex defects needs to be tuned based on your cell size—start with a value that filters out tiny gaps but keeps the obvious touch points.
  • Multi-Cell Touches: For 3+ cells touching, you'll need to cluster concave points (e.g., using K-means) to group points for each split line instead of just taking the two farthest points.
  • Contour Splitting Accuracy: The find_contour_index function uses a simple distance check—for more precision, you could use contour point interpolation or check neighboring points.

内容的提问来源于stack exchange,提问作者Vincent Liow

火山引擎 最新活动