mmarquezsa commited on
Commit
fee7e0c
·
verified ·
1 Parent(s): be49dbf

Fix: load 30 selected features from JSON instead of guessing from model

Browse files
Files changed (1) hide show
  1. src/pwat_estimator.py +21 -24
src/pwat_estimator.py CHANGED
@@ -8,6 +8,7 @@ Includes Fitzpatrick-aware debiasing correction factors.
8
  import numpy as np
9
  import cv2
10
  import joblib
 
11
  from dataclasses import dataclass, field
12
  from typing import Optional
13
  from pathlib import Path
@@ -22,7 +23,6 @@ ITEM_NAMES = {
22
  8: "Periulcer Skin",
23
  }
24
 
25
- # Debiasing correction factors (calibrated from 61 DFU images)
26
  CORRECTION_FACTORS = {
27
  "I": {3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0},
28
  "II": {3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0},
@@ -52,7 +52,6 @@ def extract_features(img_bgr: np.ndarray, ulcer_mask: np.ndarray) -> Optional[di
52
 
53
  feats = {}
54
 
55
- # Color features (45)
56
  hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.float32)
57
  lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2Lab).astype(np.float32)
58
  rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB).astype(np.float32)
@@ -66,7 +65,6 @@ def extract_features(img_bgr: np.ndarray, ulcer_mask: np.ndarray) -> Optional[di
66
  feats[f"{cs}_{cn}_p25"] = float(np.percentile(vals, 25))
67
  feats[f"{cs}_{cn}_p75"] = float(np.percentile(vals, 75))
68
 
69
- # Tissue composition (5)
70
  h, s, v = hsv[b, 0], hsv[b, 1], hsv[b, 2]
71
  l_ch = lab[b, 0] * (100 / 255)
72
  a_ch = lab[b, 1] - 128
@@ -82,7 +80,6 @@ def extract_features(img_bgr: np.ndarray, ulcer_mask: np.ndarray) -> Optional[di
82
  feats["tissue_necro_pct"] = float(np.sum(necro) / npx * 100)
83
  feats["tissue_necro_total"] = feats["tissue_eschar_pct"] + feats["tissue_slough_pct"] + feats["tissue_necro_pct"]
84
 
85
- # Morphological features (7)
86
  mask_u8 = b.astype(np.uint8) if b.dtype == bool else (ulcer_mask > 127).astype(np.uint8)
87
  cnts, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
88
  if cnts:
@@ -100,7 +97,6 @@ def extract_features(img_bgr: np.ndarray, ulcer_mask: np.ndarray) -> Optional[di
100
  hull = cv2.convexHull(cnt)
101
  feats["morph_solidity"] = float(area / (cv2.contourArea(hull) + 1e-8))
102
 
103
- # Texture features (4)
104
  gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
105
  wound_gray = gray[b]
106
  feats["texture_mean"] = float(np.mean(wound_gray))
@@ -114,7 +110,6 @@ def extract_features(img_bgr: np.ndarray, ulcer_mask: np.ndarray) -> Optional[di
114
  if np.any(edge_zone):
115
  feats["edge_gradient"] = float(np.mean(np.abs(cv2.Sobel(gray.astype(np.float32), cv2.CV_32F, 1, 0)[edge_zone])))
116
 
117
- # ROI features (2)
118
  feats["wound_npx"] = float(npx)
119
  feats["wound_ratio"] = float(npx / (img_bgr.shape[0] * img_bgr.shape[1]))
120
 
@@ -126,20 +121,22 @@ class PWATPredictor:
126
 
127
  def __init__(self, models_dir: str):
128
  self.models = {}
129
- self.feature_names = {}
130
  models_path = Path(models_dir)
 
 
 
 
 
 
 
 
 
 
 
131
  for item in ITEMS:
132
  pkl = models_path / f"xgb_pwat{item}.pkl"
133
  if pkl.exists():
134
- model = joblib.load(pkl)
135
- self.models[item] = model
136
- # Extract expected feature names from the trained model
137
- try:
138
- names = model.get_booster().feature_names
139
- if names:
140
- self.feature_names[item] = names
141
- except Exception:
142
- pass
143
 
144
  def predict(
145
  self,
@@ -152,6 +149,14 @@ class PWATPredictor:
152
  if feats is None:
153
  return PWATResult(fitzpatrick_type=fitzpatrick_type)
154
 
 
 
 
 
 
 
 
 
155
  scores_raw = {}
156
  scores_adj = {}
157
  for item in ITEMS:
@@ -160,14 +165,6 @@ class PWATPredictor:
160
  scores_adj[item] = 0.0
161
  continue
162
 
163
- # Use model's expected feature names if available
164
- if item in self.feature_names:
165
- cols = self.feature_names[item]
166
- else:
167
- cols = sorted(feats.keys())
168
-
169
- X = np.array([[feats.get(c, 0.0) for c in cols]])
170
-
171
  pred = int(self.models[item].predict(X)[0])
172
  scores_raw[item] = pred
173
  factor = CORRECTION_FACTORS.get(fitzpatrick_type, {}).get(item, 0.0)
 
8
  import numpy as np
9
  import cv2
10
  import joblib
11
+ import json
12
  from dataclasses import dataclass, field
13
  from typing import Optional
14
  from pathlib import Path
 
23
  8: "Periulcer Skin",
24
  }
25
 
 
26
  CORRECTION_FACTORS = {
27
  "I": {3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0},
28
  "II": {3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0},
 
52
 
53
  feats = {}
54
 
 
55
  hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.float32)
56
  lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2Lab).astype(np.float32)
57
  rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB).astype(np.float32)
 
65
  feats[f"{cs}_{cn}_p25"] = float(np.percentile(vals, 25))
66
  feats[f"{cs}_{cn}_p75"] = float(np.percentile(vals, 75))
67
 
 
68
  h, s, v = hsv[b, 0], hsv[b, 1], hsv[b, 2]
69
  l_ch = lab[b, 0] * (100 / 255)
70
  a_ch = lab[b, 1] - 128
 
80
  feats["tissue_necro_pct"] = float(np.sum(necro) / npx * 100)
81
  feats["tissue_necro_total"] = feats["tissue_eschar_pct"] + feats["tissue_slough_pct"] + feats["tissue_necro_pct"]
82
 
 
83
  mask_u8 = b.astype(np.uint8) if b.dtype == bool else (ulcer_mask > 127).astype(np.uint8)
84
  cnts, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
85
  if cnts:
 
97
  hull = cv2.convexHull(cnt)
98
  feats["morph_solidity"] = float(area / (cv2.contourArea(hull) + 1e-8))
99
 
 
100
  gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
101
  wound_gray = gray[b]
102
  feats["texture_mean"] = float(np.mean(wound_gray))
 
110
  if np.any(edge_zone):
111
  feats["edge_gradient"] = float(np.mean(np.abs(cv2.Sobel(gray.astype(np.float32), cv2.CV_32F, 1, 0)[edge_zone])))
112
 
 
113
  feats["wound_npx"] = float(npx)
114
  feats["wound_ratio"] = float(npx / (img_bgr.shape[0] * img_bgr.shape[1]))
115
 
 
121
 
122
  def __init__(self, models_dir: str):
123
  self.models = {}
 
124
  models_path = Path(models_dir)
125
+
126
+ # Load the selected feature columns (30 features after variance+correlation filter)
127
+ features_json = models_path / "selected_features.json"
128
+ if features_json.exists():
129
+ with open(features_json) as f:
130
+ self.selected_features = json.load(f)
131
+ print(f"PWAT: Loaded {len(self.selected_features)} selected features from JSON")
132
+ else:
133
+ self.selected_features = None
134
+ print("PWAT: WARNING — selected_features.json not found, using all features")
135
+
136
  for item in ITEMS:
137
  pkl = models_path / f"xgb_pwat{item}.pkl"
138
  if pkl.exists():
139
+ self.models[item] = joblib.load(pkl)
 
 
 
 
 
 
 
 
140
 
141
  def predict(
142
  self,
 
149
  if feats is None:
150
  return PWATResult(fitzpatrick_type=fitzpatrick_type)
151
 
152
+ # Use the exact 30 features from training (order matters)
153
+ if self.selected_features:
154
+ cols = self.selected_features
155
+ else:
156
+ cols = sorted(feats.keys())
157
+
158
+ X = np.array([[feats.get(c, 0.0) for c in cols]])
159
+
160
  scores_raw = {}
161
  scores_adj = {}
162
  for item in ITEMS:
 
165
  scores_adj[item] = 0.0
166
  continue
167
 
 
 
 
 
 
 
 
 
168
  pred = int(self.models[item].predict(X)[0])
169
  scores_raw[item] = pred
170
  factor = CORRECTION_FACTORS.get(fitzpatrick_type, {}).get(item, 0.0)