OpenCV中是否存在skimage.feature.peak_local_max的等价函数?如何实现该函数以完成纯OpenCV生物细胞分水岭分割
Hey there! Let’s tackle your problem step by step—first breaking down how skimage.feature.peak_local_max works, then building a pure OpenCV replacement that matches its behavior for your watershed segmentation workflow.
Understanding skimage.feature.peak_local_max
At its core, this function finds local peaks in your distance map (D): pixels whose value is higher than all other pixels in their surrounding neighborhood. Here’s the fine-grained breakdown of what it does with your parameters:
indices=False: Returns a binary mask where peak pixels are set to 1 (or 255 in our OpenCV version) and all others to 0.min_distance=10: Ensures no two peaks are closer than 10 pixels apart (this uses non-maximum suppression to filter out overlapping/too-close peaks).labels=thresh: Restricts peak detection only to regions wherethreshis foreground (non-zero pixels)—background areas are ignored entirely.
Under the hood, it:
- Identifies all pixels that are the maximum in their local neighborhood.
- Filters out peaks that are within
min_distanceof a brighter (higher-value) peak. - Discards any peaks that fall outside the foreground regions defined by
thresh.
Pure OpenCV Implementation of peak_local_max
We can replicate this behavior using OpenCV’s morphological operations and contour processing. Here’s a custom function that matches the scikit-image version’s output, followed by how to integrate it into your existing code:
Custom peak_local_max_opencv Function
def peak_local_max_opencv(D, min_distance=10, mask=None): # Step 1: Detect initial local maxima using morphological dilation # Create a kernel sized to cover the min_distance neighborhood kernel_size = 2 * min_distance + 1 kernel = np.ones((kernel_size, kernel_size), dtype=np.float32) # Dilate the distance map—this spreads the maximum value in each neighborhood dilated = cv2.dilate(D, kernel) # Local maxima are pixels where the original distance map equals the dilated map local_max = (D == dilated).astype(np.uint8) * 255 # Step 2: Apply foreground mask to ignore background peaks if mask is not None: local_max = cv2.bitwise_and(local_max, mask) # Step 3: Enforce min_distance between peaks with non-maximum suppression cnts = cv2.findContours(local_max.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] if len(cnts) == 0: return np.zeros_like(local_max) # Collect peak centers and their corresponding distance values peaks = [] for cnt in cnts: M = cv2.moments(cnt) if M["m00"] == 0: continue # Calculate centroid of the peak blob cX = int(M["m10"] / M["m00"]) cY = int(M["m01"] / M["m00"]) peak_value = D[cY, cX] peaks.append((peak_value, cX, cY)) # Sort peaks by their distance value (brightest first) peaks.sort(reverse=True, key=lambda x: x[0]) # Keep only peaks that are at least min_distance apart final_peaks = [] for peak in peaks: val, x, y = peak keep = True for fp in final_peaks: fx, fy = fp # Calculate Euclidean distance between peaks dist = np.sqrt((x - fx)**2 + (y - fy)**2) if dist < min_distance: keep = False break if keep: final_peaks.append((x, y)) # Generate the final peak mask output = np.zeros_like(local_max) for x, y in final_peaks: output[y, x] = 255 return output
Integrated Pure OpenCV Watershed Code
Now plug this function into your existing workflow, replacing the scikit-image calls with OpenCV equivalents:
import numpy as np import cv2 def peak_local_max_opencv(D, min_distance=10, mask=None): # Step 1: Detect initial local maxima using morphological dilation kernel_size = 2 * min_distance + 1 kernel = np.ones((kernel_size, kernel_size), dtype=np.float32) dilated = cv2.dilate(D, kernel) local_max = (D == dilated).astype(np.uint8) * 255 # Step 2: Apply foreground mask if mask is not None: local_max = cv2.bitwise_and(local_max, mask) # Step 3: Non-maximum suppression for min_distance enforcement cnts = cv2.findContours(local_max.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] if len(cnts) == 0: return np.zeros_like(local_max) peaks = [] for cnt in cnts: M = cv2.moments(cnt) if M["m00"] == 0: continue cX = int(M["m10"] / M["m00"]) cY = int(M["m01"] / M["m00"]) peak_value = D[cY, cX] peaks.append((peak_value, cX, cY)) peaks.sort(reverse=True, key=lambda x: x[0]) final_peaks = [] for peak in peaks: val, x, y = peak keep = True for fp in final_peaks: fx, fy = fp if np.sqrt((x - fx)**2 + (y - fy)**2) < min_distance: keep = False break if keep: final_peaks.append((x, y)) output = np.zeros_like(local_max) for x, y in final_peaks: output[y, x] = 255 return output # Load image and preprocess image = cv2.imread("1.png") shifted = cv2.pyrMeanShiftFiltering(image, 21, 51) gray = cv2.cvtColor(shifted, cv2.COLOR_BGR2GRAY) thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] # Compute distance transform (matches scikit-image's ndimage.distance_transform_edt) D = cv2.distanceTransform(thresh, cv2.DIST_L2, 0) # Get local peaks with our OpenCV function localMax = peak_local_max_opencv(D, min_distance=10, mask=thresh) # Connected component analysis (replaces ndimage.label) # Use connectivity=8 to match scikit-image's default 8-connectivity num_labels, markers = cv2.connectedComponents(localMax, connectivity=8) # Apply OpenCV's watershed algorithm # Note: OpenCV's watershed marks boundary pixels as -1, so we'll skip those later labels = cv2.watershed(image, markers) print("[INFO] {} unique segments found".format(len(np.unique(labels)) - 2)) # Subtract background (0) and boundaries (-1) # Save label map (convert to uint8 for valid image output) cv2.imwrite("labels.png", (labels + 1).astype(np.uint8)) # Shift values to avoid negative numbers # Draw contours around segmented cells for label in np.unique(labels): # Skip background (0) and watershed boundaries (-1) if label == 0 or label == -1: continue # Create mask for current label mask = np.zeros(gray.shape, dtype="uint8") mask[labels == label] = 255 # Find and draw largest contour cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] c = max(cnts, key=cv2.contourArea) peri = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.018 * peri, True) cv2.drawContours(image, [approx], -1, (0, 0, 255), 2) # Save final segmented image cv2.imwrite("output.jpg", image)
Key Notes for Matching Scikit-Image Behavior
- Morphological Dilation: This replicates how scikit-image identifies local maxima—only pixels that are the highest in their
min_distanceneighborhood remain after comparing the original and dilated distance maps. - Non-Maximum Suppression: Ensures peaks are at least
min_distanceapart, just like scikit-image’s parameter. We sort peaks by brightness first to keep the strongest peaks. - Connected Components: Using
cv2.connectedComponentswithconnectivity=8matchesndimage.label’s default 8-connectivity. - OpenCV Watershed: Note that OpenCV’s watershed marks boundary pixels as
-1, so we skip those in our contour loop.
内容的提问来源于stack exchange,提问作者fecund_geek

