mmarquezsa commited on
Commit
8b7a837
·
verified ·
1 Parent(s): 8541ab5

feat: lighting quality assessment for Fitzpatrick estimation

Browse files
Files changed (1) hide show
  1. src/fitzpatrick_estimator.py +72 -4
src/fitzpatrick_estimator.py CHANGED
@@ -2,6 +2,9 @@
2
 
3
  Calibrated on 61 DFU images with expert ground truth.
4
  Validation: 86.9% exact match, 98.4% adjacent, r=0.975.
 
 
 
5
  """
6
  import numpy as np
7
  import cv2
@@ -23,6 +26,12 @@ FITZPATRICK_LABELS = {
23
  "IV": "Tan", "V": "Brown", "VI": "Dark",
24
  }
25
 
 
 
 
 
 
 
26
 
27
  @dataclass
28
  class FitzpatrickResult:
@@ -36,6 +45,10 @@ class FitzpatrickResult:
36
  healthy_pixels: int # Number of healthy skin pixels used
37
  healthy_ratio: float # Healthy pixels / total image pixels
38
  confidence: float # 0-1 confidence score
 
 
 
 
39
 
40
 
41
  def compute_ita(l_values: np.ndarray, b_values: np.ndarray) -> tuple:
@@ -62,6 +75,43 @@ def classify_fitzpatrick(ita: float) -> tuple:
62
  return "III", 3 # Default fallback
63
 
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  def estimate_fitzpatrick(
66
  img_bgr: np.ndarray,
67
  masks: dict,
@@ -72,6 +122,9 @@ def estimate_fitzpatrick(
72
  Strategy: Healthy skin = foot region - perilesion zone - ulcer.
73
  ITA is computed on the healthy skin pixels only.
74
 
 
 
 
75
  Args:
76
  img_bgr: BGR image (H, W, 3)
77
  masks: dict with keys 'foot', 'perilesion', 'ulcer' (bool arrays H, W)
@@ -107,6 +160,9 @@ def estimate_fitzpatrick(
107
  healthy_pixels=healthy_pixels,
108
  healthy_ratio=healthy_pixels / (h * w),
109
  confidence=0.0,
 
 
 
110
  )
111
 
112
  # Convert to L*a*b*
@@ -117,11 +173,20 @@ def estimate_fitzpatrick(
117
  ita_mean, ita_std = compute_ita(l_values, b_values)
118
  ftype, fint = classify_fitzpatrick(ita_mean)
119
 
120
- # Confidence: pixel count + ITA consistency + coverage
 
 
 
 
 
 
 
 
121
  pixel_conf = min(healthy_pixels / 5000.0, 1.0)
122
  ita_conf = max(0.0, 1.0 - (ita_std / 30.0))
123
  coverage_conf = min((healthy_pixels / (h * w)) / 0.15, 1.0)
124
- confidence = pixel_conf * 0.3 + ita_conf * 0.4 + coverage_conf * 0.3
 
125
 
126
  return FitzpatrickResult(
127
  fitzpatrick_type=ftype,
@@ -129,9 +194,12 @@ def estimate_fitzpatrick(
129
  fitzpatrick_label=FITZPATRICK_LABELS[ftype],
130
  ita_angle=round(ita_mean, 2),
131
  ita_std=round(ita_std, 2),
132
- l_skin_mean=round(float(np.mean(l_values)), 2),
133
- b_skin_mean=round(float(np.mean(b_values)), 2),
134
  healthy_pixels=healthy_pixels,
135
  healthy_ratio=round(healthy_pixels / (h * w), 4),
136
  confidence=round(confidence, 3),
 
 
 
137
  )
 
2
 
3
  Calibrated on 61 DFU images with expert ground truth.
4
  Validation: 86.9% exact match, 98.4% adjacent, r=0.975.
5
+
6
+ Includes lighting quality assessment to avoid misclassification
7
+ under poor illumination conditions.
8
  """
9
  import numpy as np
10
  import cv2
 
26
  "IV": "Tan", "V": "Brown", "VI": "Dark",
27
  }
28
 
29
+ # Lighting quality thresholds (L* scale 0-100)
30
+ L_SCENE_MIN = 35.0 # Below this: scene too dark for reliable ITA
31
+ L_SCENE_LOW = 50.0 # Below this: suboptimal lighting, reduce confidence
32
+ L_SKIN_MIN = 25.0 # Healthy skin L* below this is almost certainly lighting artifact
33
+ L_SKIN_SUSPICIOUS = 40.0 # L* this low is rare even for Fitzpatrick VI under good light
34
+
35
 
36
  @dataclass
37
  class FitzpatrickResult:
 
45
  healthy_pixels: int # Number of healthy skin pixels used
46
  healthy_ratio: float # Healthy pixels / total image pixels
47
  confidence: float # 0-1 confidence score
48
+ # Lighting quality
49
+ l_scene_mean: float = 0.0 # Mean L* of entire image
50
+ lighting_quality: str = "good" # "good", "low", "insufficient"
51
+ lighting_warning: str = "" # Human-readable warning if lighting is poor
52
 
53
 
54
  def compute_ita(l_values: np.ndarray, b_values: np.ndarray) -> tuple:
 
75
  return "III", 3 # Default fallback
76
 
77
 
78
+ def assess_lighting(img_bgr: np.ndarray, l_skin_mean: float) -> tuple:
79
+ """Assess scene lighting quality for reliable Fitzpatrick estimation.
80
+
81
+ Returns:
82
+ (l_scene_mean, quality, warning, confidence_penalty)
83
+ quality: "good", "low", "insufficient"
84
+ confidence_penalty: 0.0 to 1.0 (multiplied with confidence)
85
+ """
86
+ # Global scene luminance
87
+ lab_full = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2Lab).astype(np.float32)
88
+ l_scene = float(np.mean(lab_full[:, :, 0]) * (100.0 / 255.0))
89
+
90
+ # Evaluate lighting
91
+ if l_scene < L_SCENE_MIN or l_skin_mean < L_SKIN_MIN:
92
+ quality = "insufficient"
93
+ warning = (
94
+ f"Iluminacion insuficiente (L* escena={l_scene:.0f}, L* piel={l_skin_mean:.0f}). "
95
+ "El tipo Fitzpatrick puede estar sobreestimado (piel aparenta mas oscura). "
96
+ "Recapture con mejor iluminacion para un resultado confiable."
97
+ )
98
+ penalty = 0.2 # Severely reduce confidence
99
+ elif l_scene < L_SCENE_LOW or l_skin_mean < L_SKIN_SUSPICIOUS:
100
+ quality = "low"
101
+ warning = (
102
+ f"Iluminacion suboptima (L* escena={l_scene:.0f}, L* piel={l_skin_mean:.0f}). "
103
+ "El tipo Fitzpatrick podria estar 1-2 niveles sobreestimado. "
104
+ "Se recomienda iluminacion uniforme para mayor precision."
105
+ )
106
+ penalty = 0.5 # Moderate confidence reduction
107
+ else:
108
+ quality = "good"
109
+ warning = ""
110
+ penalty = 1.0 # No penalty
111
+
112
+ return l_scene, quality, warning, penalty
113
+
114
+
115
  def estimate_fitzpatrick(
116
  img_bgr: np.ndarray,
117
  masks: dict,
 
122
  Strategy: Healthy skin = foot region - perilesion zone - ulcer.
123
  ITA is computed on the healthy skin pixels only.
124
 
125
+ Includes lighting quality assessment — if scene is too dark,
126
+ confidence is penalized and a warning is issued.
127
+
128
  Args:
129
  img_bgr: BGR image (H, W, 3)
130
  masks: dict with keys 'foot', 'perilesion', 'ulcer' (bool arrays H, W)
 
160
  healthy_pixels=healthy_pixels,
161
  healthy_ratio=healthy_pixels / (h * w),
162
  confidence=0.0,
163
+ l_scene_mean=0.0,
164
+ lighting_quality="insufficient",
165
+ lighting_warning="Insuficientes pixeles de piel sana para estimar Fitzpatrick.",
166
  )
167
 
168
  # Convert to L*a*b*
 
173
  ita_mean, ita_std = compute_ita(l_values, b_values)
174
  ftype, fint = classify_fitzpatrick(ita_mean)
175
 
176
+ l_skin_mean = float(np.mean(l_values))
177
+ b_skin_mean = float(np.mean(b_values))
178
+
179
+ # Lighting quality assessment
180
+ l_scene, lighting_quality, lighting_warning, lighting_penalty = assess_lighting(
181
+ img_bgr, l_skin_mean
182
+ )
183
+
184
+ # Confidence: pixel count + ITA consistency + coverage + lighting
185
  pixel_conf = min(healthy_pixels / 5000.0, 1.0)
186
  ita_conf = max(0.0, 1.0 - (ita_std / 30.0))
187
  coverage_conf = min((healthy_pixels / (h * w)) / 0.15, 1.0)
188
+ base_confidence = pixel_conf * 0.3 + ita_conf * 0.4 + coverage_conf * 0.3
189
+ confidence = base_confidence * lighting_penalty
190
 
191
  return FitzpatrickResult(
192
  fitzpatrick_type=ftype,
 
194
  fitzpatrick_label=FITZPATRICK_LABELS[ftype],
195
  ita_angle=round(ita_mean, 2),
196
  ita_std=round(ita_std, 2),
197
+ l_skin_mean=round(l_skin_mean, 2),
198
+ b_skin_mean=round(b_skin_mean, 2),
199
  healthy_pixels=healthy_pixels,
200
  healthy_ratio=round(healthy_pixels / (h * w), 4),
201
  confidence=round(confidence, 3),
202
+ l_scene_mean=round(l_scene, 2),
203
+ lighting_quality=lighting_quality,
204
+ lighting_warning=lighting_warning,
205
  )