如何提取检测凹点并拟合椭圆?触摸细胞分割技术求助
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_thresholdfor 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_indexfunction uses a simple distance check—for more precision, you could use contour point interpolation or check neighboring points.
内容的提问来源于stack exchange,提问作者Vincent Liow




