fix: preserve dark necrotic tissue connected to foot, only remove isolated dark blobs
Browse files- src/segmentation.py +21 -20
src/segmentation.py
CHANGED
|
@@ -261,16 +261,20 @@ def postprocess_segmentation(
|
|
| 261 |
classmap: np.ndarray,
|
| 262 |
img_bgr: np.ndarray,
|
| 263 |
min_foot_ratio: float = 0.01,
|
| 264 |
-
dark_l_threshold: float =
|
| 265 |
) -> np.ndarray:
|
| 266 |
"""Post-process segmentation to remove noise and dark-background misclassifications.
|
| 267 |
|
| 268 |
Steps:
|
| 269 |
1. Keep only the largest connected component of foreground (foot+peri+ulcer).
|
| 270 |
Small disconnected blobs are reclassified as background.
|
| 271 |
-
2. Exclude very dark pixels
|
| 272 |
-
|
| 273 |
3. Morphological opening to clean noisy edges.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
"""
|
| 275 |
h, w = classmap.shape
|
| 276 |
cleaned = classmap.copy()
|
|
@@ -279,11 +283,13 @@ def postprocess_segmentation(
|
|
| 279 |
foreground = (cleaned > 0).astype(np.uint8)
|
| 280 |
num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(foreground, connectivity=8)
|
| 281 |
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
| 287 |
min_area = h * w * min_foot_ratio
|
| 288 |
|
| 289 |
for label_id in range(1, num_labels):
|
|
@@ -291,26 +297,21 @@ def postprocess_segmentation(
|
|
| 291 |
continue
|
| 292 |
component_area = stats[label_id, cv2.CC_STAT_AREA]
|
| 293 |
if component_area < min_area:
|
| 294 |
-
# Small disconnected blob → background
|
| 295 |
cleaned[labels == label_id] = 0
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
# Step 2: Dark pixel exclusion
|
| 298 |
-
#
|
| 299 |
lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2Lab).astype(np.float32)
|
| 300 |
l_channel = lab[:, :, 0] * (100.0 / 255.0)
|
| 301 |
|
| 302 |
dark_mask = l_channel < dark_l_threshold
|
| 303 |
is_foreground = cleaned > 0
|
| 304 |
|
| 305 |
-
# Only
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
if np.any(core_foreground):
|
| 309 |
-
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (31, 31))
|
| 310 |
-
core_dilated = cv2.dilate(core_foreground, kernel).astype(bool)
|
| 311 |
-
# Dark foreground pixels NOT touching the core → background
|
| 312 |
-
dark_isolated = dark_mask & is_foreground & ~core_dilated
|
| 313 |
-
cleaned[dark_isolated] = 0
|
| 314 |
|
| 315 |
# Step 3: Morphological opening per foreground class to clean edges
|
| 316 |
kernel_small = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
|
|
|
| 261 |
classmap: np.ndarray,
|
| 262 |
img_bgr: np.ndarray,
|
| 263 |
min_foot_ratio: float = 0.01,
|
| 264 |
+
dark_l_threshold: float = 15.0,
|
| 265 |
) -> np.ndarray:
|
| 266 |
"""Post-process segmentation to remove noise and dark-background misclassifications.
|
| 267 |
|
| 268 |
Steps:
|
| 269 |
1. Keep only the largest connected component of foreground (foot+peri+ulcer).
|
| 270 |
Small disconnected blobs are reclassified as background.
|
| 271 |
+
2. Exclude very dark pixels ONLY if they are NOT part of the main connected
|
| 272 |
+
foreground component. Dark necrotic tissue connected to the foot is preserved.
|
| 273 |
3. Morphological opening to clean noisy edges.
|
| 274 |
+
|
| 275 |
+
Key design: necrotic tissue (very dark, low L*) that is connected to the main
|
| 276 |
+
foot region is NEVER removed — only isolated dark blobs disconnected from the
|
| 277 |
+
foot are reclassified as background.
|
| 278 |
"""
|
| 279 |
h, w = classmap.shape
|
| 280 |
cleaned = classmap.copy()
|
|
|
|
| 283 |
foreground = (cleaned > 0).astype(np.uint8)
|
| 284 |
num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(foreground, connectivity=8)
|
| 285 |
|
| 286 |
+
# Track which pixels belong to the main component
|
| 287 |
+
main_component_mask = np.zeros((h, w), dtype=bool)
|
| 288 |
+
|
| 289 |
+
if num_labels > 1:
|
| 290 |
+
areas = stats[1:, cv2.CC_STAT_AREA]
|
| 291 |
+
largest_label = np.argmax(areas) + 1
|
| 292 |
+
main_component_mask = (labels == largest_label)
|
| 293 |
min_area = h * w * min_foot_ratio
|
| 294 |
|
| 295 |
for label_id in range(1, num_labels):
|
|
|
|
| 297 |
continue
|
| 298 |
component_area = stats[label_id, cv2.CC_STAT_AREA]
|
| 299 |
if component_area < min_area:
|
|
|
|
| 300 |
cleaned[labels == label_id] = 0
|
| 301 |
+
else:
|
| 302 |
+
main_component_mask = foreground.astype(bool)
|
| 303 |
|
| 304 |
+
# Step 2: Dark pixel exclusion — ONLY for pixels NOT in the main component
|
| 305 |
+
# Necrotic tissue connected to the foot is preserved regardless of darkness
|
| 306 |
lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2Lab).astype(np.float32)
|
| 307 |
l_channel = lab[:, :, 0] * (100.0 / 255.0)
|
| 308 |
|
| 309 |
dark_mask = l_channel < dark_l_threshold
|
| 310 |
is_foreground = cleaned > 0
|
| 311 |
|
| 312 |
+
# Only remove dark pixels that are foreground but NOT in the main component
|
| 313 |
+
dark_isolated = dark_mask & is_foreground & ~main_component_mask
|
| 314 |
+
cleaned[dark_isolated] = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
# Step 3: Morphological opening per foreground class to clean edges
|
| 317 |
kernel_small = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|