Source code for acia.segm.processor.canny
"""Canny edge detection based segmentation processor"""
import cv2
import numpy as np
from tqdm.auto import tqdm
from acia.base import Contour, ImageSequenceSource, Overlay
from . import SegmentationProcessor
[docs]
class CannySegmentationProcessor(SegmentationProcessor):
"""Segmentation processor using Canny edge detection.
This processor uses OpenCV's Canny edge detection algorithm to identify
cell boundaries in images, then extracts contours from the edge map.
Args:
canny_low (int): Lower threshold for Canny edge detection. Default: 50
canny_high (int): Upper threshold for Canny edge detection. Default: 150
min_area (float): Minimum contour area in pixels. Contours smaller than
this are filtered out. Default: 50
max_area (float): Maximum contour area in pixels. Contours larger than
this are filtered out. Default: 100000
blur_kernel (int): Size of Gaussian blur kernel (must be odd). Default: 5
"""
[docs]
def __init__(
self,
canny_low: int = 50,
canny_high: int = 150,
min_area: float = 50,
max_area: float = 100000,
blur_kernel: int = 5,
):
"""Initialize Canny edge detection processor with configurable parameters."""
self.canny_low = canny_low
self.canny_high = canny_high
self.min_area = min_area
self.max_area = max_area
self.blur_kernel = blur_kernel if blur_kernel % 2 == 1 else blur_kernel + 1
def __call__(self, images: ImageSequenceSource) -> Overlay:
"""Process image sequence and return segmentation overlay.
Args:
images (ImageSequenceSource): Source of images to segment
Returns:
Overlay: Overlay containing detected contours with correct frame indices
"""
overlay = Overlay([])
for frame_id, image in enumerate(
tqdm(images, desc="Performing Canny edge detection segmentation...")
):
# Extract raw image
img = image.raw
# Convert to grayscale if needed
if len(img.shape) == 3:
# Handle multi-channel image
if img.shape[2] == 1:
# Single channel, just squeeze
gray = img[..., 0]
else:
# Multiple channels, convert from BGR to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
# Already grayscale
gray = img
# Ensure uint8 format for Canny
if gray.dtype != np.uint8:
gray = cv2.convertScaleAbs(gray)
# Apply Gaussian blur to reduce noise
blurred = cv2.GaussianBlur(gray, (self.blur_kernel, self.blur_kernel), 1.5)
# Apply Canny edge detection
edges = cv2.Canny(blurred, self.canny_low, self.canny_high)
# Find contours from edge map
contours, _ = cv2.findContours(
edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
)
# Process and filter contours
for contour in contours:
# Squeeze contour array to remove singleton dimension
contour = np.squeeze(contour)
# Skip degenerate contours (need at least 3 points for a valid polygon)
if contour.ndim < 2 or len(contour) < 3:
continue
# Calculate contour area
area = cv2.contourArea(contour)
# Filter by area thresholds
if area < self.min_area or area > self.max_area:
continue
# Calculate perimeter for validation
perimeter = cv2.arcLength(contour, True)
if perimeter <= 0:
continue
# Optional: Calculate solidity (area / convex hull area)
# This filters for well-shaped contours
hull = cv2.convexHull(contour)
hull_area = cv2.contourArea(hull)
if hull_area > 0:
solidity = float(area) / hull_area
if solidity < 0.5:
# Skip poorly shaped contours
continue
# Create contour object with area as the score
# Ensure contour is float32 and in (x, y) format
contour_float = contour.astype(np.float32)
contour_obj = Contour(
coordinates=contour_float,
score=area,
frame=frame_id,
id=-1,
label=None,
)
overlay.add_contour(contour_obj)
return overlay