mmarquezsa commited on
Commit
262e4a4
·
verified ·
1 Parent(s): 7926585

fix: preserve dark necrotic tissue connected to foot, only remove isolated dark blobs

Browse files
Files changed (1) hide show
  1. 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 = 20.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 (L* < threshold) that are far from the main
272
- foot region these are likely background/shadow, not skin or wound.
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
- if num_labels > 2:
283
- # Find the largest foreground component (label 0 is background in connectedComponents)
284
- # stats[:, cv2.CC_STAT_AREA] gives area of each label
285
- areas = stats[1:, cv2.CC_STAT_AREA] # Skip background (label 0)
286
- largest_label = np.argmax(areas) + 1 # +1 because we skipped label 0
 
 
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
- # Very dark pixels that ended up as foreground are likely background/shadow
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 reclassify dark pixels that are NOT adjacent to the main foot core
306
- # Strategy: dilate the non-dark foreground region, dark pixels outside it → background
307
- core_foreground = (is_foreground & ~dark_mask).astype(np.uint8)
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))