mmarquezsa commited on
Commit
9a9c395
·
verified ·
1 Parent(s): 9235649

fix: iterative necrotic recovery (3x30px) — bridges gap to necrotic toes

Browse files
Files changed (1) hide show
  1. src/segmentation.py +53 -38
src/segmentation.py CHANGED
@@ -331,29 +331,30 @@ def recover_necrotic_tissue(
331
  l_channel: np.ndarray,
332
  a_channel: np.ndarray,
333
  s_channel: np.ndarray,
334
- necrotic_l_max: float = 35.0,
335
- necrotic_s_max: float = 80.0,
336
- dilation_px: int = 40,
337
- min_region_px: int = 200,
338
  ) -> np.ndarray:
339
  """Recover dark necrotic tissue regions adjacent to detected foreground.
340
 
341
- Necrotic tissue (eschar, gangrene) is very dark (low L*) and low saturation,
342
- which the segmentation model often misclassifies as background.
343
- This function detects such regions adjacent to the foot and reclassifies
344
- them as ulcer (class 3).
345
 
346
- Detection criteria for necrotic pixels:
347
- - L* < 35 (very dark)
348
- - Saturation < 80 (not vivid colored — rules out colored backgrounds)
349
  - Currently classified as background (class 0)
350
- - Adjacent to (within dilation_px of) detected foreground
351
- - Forms a connected region >= min_region_px pixels
 
 
352
  """
353
  h, w = classmap.shape
354
  recovered = classmap.copy()
355
 
356
- # Candidate necrotic pixels: dark, low saturation, currently background
357
  is_background = recovered == 0
358
  necrotic_candidates = (
359
  is_background
@@ -364,36 +365,50 @@ def recover_necrotic_tissue(
364
  if not np.any(necrotic_candidates):
365
  return recovered
366
 
367
- # Adjacency: dilate the current foreground to find nearby regions
368
- foreground = (recovered > 0).astype(np.uint8)
369
- kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (dilation_px, dilation_px))
370
- foreground_dilated = cv2.dilate(foreground, kernel).astype(bool)
 
 
371
 
372
- # Keep only candidates adjacent to detected foot
373
- adjacent_necrotic = necrotic_candidates & foreground_dilated
374
 
375
- if not np.any(adjacent_necrotic):
376
- return recovered
 
377
 
378
- # Connected component filtering: only keep regions large enough to be real tissue
379
- adjacent_u8 = adjacent_necrotic.astype(np.uint8)
380
- num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(adjacent_u8, connectivity=8)
381
 
382
- for label_id in range(1, num_labels):
383
- area = stats[label_id, cv2.CC_STAT_AREA]
384
- if area >= min_region_px:
385
- # This region is large enough to be real necrotic tissue
386
- region_mask = labels == label_id
 
 
 
 
 
 
 
387
 
388
- # Verify it's actually connected to the foreground (not just nearby)
389
- # Check if dilating this region touches existing foreground
390
- region_u8 = region_mask.astype(np.uint8)
391
- region_dilated = cv2.dilate(region_u8, cv2.getStructuringElement(
392
- cv2.MORPH_ELLIPSE, (5, 5)
393
- ))
394
- touches_foreground = np.any((region_dilated > 0) & (foreground > 0))
395
 
396
- if touches_foreground:
 
 
 
 
 
397
  recovered[region_mask] = 3 # Ulcer (necrotic)
 
 
 
 
 
 
 
398
 
399
  return recovered
 
331
  l_channel: np.ndarray,
332
  a_channel: np.ndarray,
333
  s_channel: np.ndarray,
334
+ necrotic_l_max: float = 45.0,
335
+ necrotic_s_max: float = 120.0,
336
+ min_region_px: int = 100,
 
337
  ) -> np.ndarray:
338
  """Recover dark necrotic tissue regions adjacent to detected foreground.
339
 
340
+ Necrotic tissue (eschar, gangrene, dry/wet gangrene on toes) is very dark
341
+ and the model often misclassifies it as background. This function uses
342
+ iterative dilation to progressively recover necrotic regions connected
343
+ to the foot, even when there's a gap between the detected foot and the toes.
344
 
345
+ Detection criteria for necrotic candidate pixels:
346
+ - L* < 45 (dark tissue — covers eschar, gangrene, necrotic toes)
347
+ - Saturation < 120 (not vivid colored — rules out green/blue backgrounds)
348
  - Currently classified as background (class 0)
349
+
350
+ Iterative approach: dilate foreground progressively (3 rounds x 30px),
351
+ recovering necrotic candidates at each step. This bridges gaps between
352
+ the detected foot and disconnected necrotic regions like toes.
353
  """
354
  h, w = classmap.shape
355
  recovered = classmap.copy()
356
 
357
+ # Candidate necrotic pixels: dark, not vivid, currently background
358
  is_background = recovered == 0
359
  necrotic_candidates = (
360
  is_background
 
365
  if not np.any(necrotic_candidates):
366
  return recovered
367
 
368
+ # Iterative recovery: progressively expand from detected foreground
369
+ # Each round dilates 30px and recovers adjacent necrotic tissue,
370
+ # then the recovered tissue becomes part of the foreground for the next round.
371
+ # 3 rounds × 30px = up to 90px reach from the original foreground edge.
372
+ dilation_step = 30
373
+ num_rounds = 3
374
 
375
+ current_foreground = (recovered > 0).astype(np.uint8)
 
376
 
377
+ for round_idx in range(num_rounds):
378
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (dilation_step, dilation_step))
379
+ fg_dilated = cv2.dilate(current_foreground, kernel).astype(bool)
380
 
381
+ # Candidates that are within reach this round
382
+ adjacent = necrotic_candidates & fg_dilated & (recovered == 0)
 
383
 
384
+ if not np.any(adjacent):
385
+ break
386
+
387
+ # Connected component filtering
388
+ adjacent_u8 = adjacent.astype(np.uint8)
389
+ num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(adjacent_u8, connectivity=8)
390
+
391
+ recovered_any = False
392
+ for label_id in range(1, num_labels):
393
+ area = stats[label_id, cv2.CC_STAT_AREA]
394
+ if area < min_region_px:
395
+ continue
396
 
397
+ region_mask = labels == label_id
 
 
 
 
 
 
398
 
399
+ # Verify it touches current foreground
400
+ region_dilated = cv2.dilate(
401
+ region_mask.astype(np.uint8),
402
+ cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
403
+ )
404
+ if np.any((region_dilated > 0) & (current_foreground > 0)):
405
  recovered[region_mask] = 3 # Ulcer (necrotic)
406
+ recovered_any = True
407
+
408
+ if not recovered_any:
409
+ break
410
+
411
+ # Update foreground for next round (include newly recovered tissue)
412
+ current_foreground = (recovered > 0).astype(np.uint8)
413
 
414
  return recovered