Fix: load 30 selected features from JSON instead of guessing from model
Browse files- 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 |
-
|
| 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)
|