Spaces:
Sleeping
Sleeping
Update src/ai_processor.py
Browse files- src/ai_processor.py +75 -27
src/ai_processor.py
CHANGED
|
@@ -1223,11 +1223,10 @@ Automated analysis provides quantitative measurements; verify via clinical exami
|
|
| 1223 |
visual = result.get("visual_analysis", {}) or {}
|
| 1224 |
px_per_cm = float(visual.get("px_per_cm", DEFAULT_PX_PER_CM))
|
| 1225 |
|
| 1226 |
-
# Attempt to load
|
| 1227 |
roi_mask_path = visual.get("roi_mask_path")
|
| 1228 |
mask_img = None
|
| 1229 |
|
| 1230 |
-
# Use manual mask if provided
|
| 1231 |
if manual_mask_path:
|
| 1232 |
try:
|
| 1233 |
if os.path.exists(manual_mask_path):
|
|
@@ -1237,91 +1236,140 @@ Automated analysis provides quantitative measurements; verify via clinical exami
|
|
| 1237 |
except Exception as e:
|
| 1238 |
logging.warning(f"Failed to load manual mask: {e}")
|
| 1239 |
elif roi_mask_path and os.path.exists(roi_mask_path):
|
| 1240 |
-
# Otherwise load the automatically generated ROI mask
|
| 1241 |
try:
|
| 1242 |
mask_img = Image.open(roi_mask_path)
|
| 1243 |
except Exception as e:
|
| 1244 |
logging.warning(f"Failed to load ROI mask for adjustment: {e}")
|
| 1245 |
|
| 1246 |
if mask_img is not None:
|
| 1247 |
-
# Convert to numpy for processing
|
| 1248 |
mask_np = np.array(mask_img.convert("L"))
|
| 1249 |
|
| 1250 |
-
# If adjustment
|
| 1251 |
if (manual_mask_path is None) and (abs(seg_adjust) >= 1e-5):
|
| 1252 |
-
# Determine the number of iterations based on percentage; roughly 5% increments
|
| 1253 |
iter_count = max(1, int(round(abs(seg_adjust) / 5)))
|
| 1254 |
kernel = np.ones((3, 3), np.uint8)
|
| 1255 |
try:
|
| 1256 |
if seg_adjust > 0:
|
| 1257 |
-
# Dilate (expand) mask
|
| 1258 |
mask_np = cv2.dilate((mask_np > 127).astype(np.uint8), kernel, iterations=iter_count)
|
| 1259 |
else:
|
| 1260 |
-
# Erode (shrink) mask
|
| 1261 |
mask_np = cv2.erode((mask_np > 127).astype(np.uint8), kernel, iterations=iter_count)
|
| 1262 |
except Exception as e:
|
| 1263 |
logging.warning(f"Segmentation adjustment failed: {e}")
|
| 1264 |
else:
|
| 1265 |
-
# If manual mask provided, binarize it directly
|
| 1266 |
mask_np = (mask_np > 127).astype(np.uint8)
|
| 1267 |
|
| 1268 |
-
# Recalculate
|
| 1269 |
try:
|
| 1270 |
length_cm, breadth_cm, area_cm2 = self._refine_metrics_from_mask(mask_np, px_per_cm)
|
| 1271 |
visual["length_cm"] = length_cm
|
| 1272 |
visual["breadth_cm"] = breadth_cm
|
| 1273 |
visual["surface_area_cm2"] = area_cm2
|
| 1274 |
-
# Indicate that segmentation was refined manually or adjusted
|
| 1275 |
visual["segmentation_refined"] = bool(manual_mask_path) or (abs(seg_adjust) >= 1e-5)
|
| 1276 |
except Exception as e:
|
| 1277 |
logging.warning(f"Failed to recalculate metrics from mask: {e}")
|
| 1278 |
|
| 1279 |
-
#
|
| 1280 |
if manual_mask_path:
|
| 1281 |
try:
|
| 1282 |
-
# Base image for overlay
|
| 1283 |
base_rgb = np.array(image_pil.convert("RGB"))
|
| 1284 |
base_bgr = cv2.cvtColor(base_rgb, cv2.COLOR_RGB2BGR)
|
| 1285 |
h, w = base_bgr.shape[:2]
|
| 1286 |
|
| 1287 |
-
# Ensure mask matches base size
|
| 1288 |
if mask_np.shape[:2] != (h, w):
|
| 1289 |
mask_np = cv2.resize(mask_np.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST)
|
| 1290 |
|
| 1291 |
-
#
|
| 1292 |
-
|
| 1293 |
-
if
|
| 1294 |
-
|
| 1295 |
|
|
|
|
|
|
|
| 1296 |
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1297 |
|
| 1298 |
-
# Save
|
| 1299 |
manual_mask_save = os.path.join(out_dir, f"manual_mask_{ts}.png")
|
| 1300 |
cv2.imwrite(manual_mask_save, (mask_np * 255).astype(np.uint8))
|
| 1301 |
|
| 1302 |
-
#
|
| 1303 |
red = np.zeros_like(base_bgr); red[:] = (0, 0, 255)
|
| 1304 |
alpha = 0.55
|
| 1305 |
tinted = cv2.addWeighted(base_bgr, 1 - alpha, red, alpha, 0)
|
| 1306 |
-
|
|
|
|
| 1307 |
overlay = np.where(mask3 > 0, tinted, base_bgr)
|
| 1308 |
|
| 1309 |
-
|
| 1310 |
-
|
| 1311 |
-
if
|
| 1312 |
-
|
|
|
|
|
|
|
|
|
|
| 1313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1314 |
manual_overlay_path = os.path.join(out_dir, f"segmentation_manual_{ts}.png")
|
| 1315 |
cv2.imwrite(manual_overlay_path, overlay)
|
| 1316 |
|
| 1317 |
-
# Update paths so
|
| 1318 |
visual["roi_mask_path"] = manual_mask_save
|
| 1319 |
visual["segmentation_image_path"] = manual_overlay_path
|
| 1320 |
-
visual["segmentation_roi_path"] = manual_overlay_path
|
|
|
|
| 1321 |
visual["segmentation_refined_type"] = "manual"
|
| 1322 |
visual["manual_mask_used"] = True
|
| 1323 |
except Exception as e:
|
| 1324 |
logging.warning(f"Failed to generate manual segmentation overlay: {e}")
|
|
|
|
| 1325 |
|
| 1326 |
result["visual_analysis"] = visual
|
| 1327 |
return result
|
|
|
|
| 1223 |
visual = result.get("visual_analysis", {}) or {}
|
| 1224 |
px_per_cm = float(visual.get("px_per_cm", DEFAULT_PX_PER_CM))
|
| 1225 |
|
| 1226 |
+
# Attempt to load a mask
|
| 1227 |
roi_mask_path = visual.get("roi_mask_path")
|
| 1228 |
mask_img = None
|
| 1229 |
|
|
|
|
| 1230 |
if manual_mask_path:
|
| 1231 |
try:
|
| 1232 |
if os.path.exists(manual_mask_path):
|
|
|
|
| 1236 |
except Exception as e:
|
| 1237 |
logging.warning(f"Failed to load manual mask: {e}")
|
| 1238 |
elif roi_mask_path and os.path.exists(roi_mask_path):
|
|
|
|
| 1239 |
try:
|
| 1240 |
mask_img = Image.open(roi_mask_path)
|
| 1241 |
except Exception as e:
|
| 1242 |
logging.warning(f"Failed to load ROI mask for adjustment: {e}")
|
| 1243 |
|
| 1244 |
if mask_img is not None:
|
|
|
|
| 1245 |
mask_np = np.array(mask_img.convert("L"))
|
| 1246 |
|
| 1247 |
+
# If adjustment requested and no manual override
|
| 1248 |
if (manual_mask_path is None) and (abs(seg_adjust) >= 1e-5):
|
|
|
|
| 1249 |
iter_count = max(1, int(round(abs(seg_adjust) / 5)))
|
| 1250 |
kernel = np.ones((3, 3), np.uint8)
|
| 1251 |
try:
|
| 1252 |
if seg_adjust > 0:
|
|
|
|
| 1253 |
mask_np = cv2.dilate((mask_np > 127).astype(np.uint8), kernel, iterations=iter_count)
|
| 1254 |
else:
|
|
|
|
| 1255 |
mask_np = cv2.erode((mask_np > 127).astype(np.uint8), kernel, iterations=iter_count)
|
| 1256 |
except Exception as e:
|
| 1257 |
logging.warning(f"Segmentation adjustment failed: {e}")
|
| 1258 |
else:
|
|
|
|
| 1259 |
mask_np = (mask_np > 127).astype(np.uint8)
|
| 1260 |
|
| 1261 |
+
# Recalculate metrics
|
| 1262 |
try:
|
| 1263 |
length_cm, breadth_cm, area_cm2 = self._refine_metrics_from_mask(mask_np, px_per_cm)
|
| 1264 |
visual["length_cm"] = length_cm
|
| 1265 |
visual["breadth_cm"] = breadth_cm
|
| 1266 |
visual["surface_area_cm2"] = area_cm2
|
|
|
|
| 1267 |
visual["segmentation_refined"] = bool(manual_mask_path) or (abs(seg_adjust) >= 1e-5)
|
| 1268 |
except Exception as e:
|
| 1269 |
logging.warning(f"Failed to recalculate metrics from mask: {e}")
|
| 1270 |
|
| 1271 |
+
# ------- Manual overlay with wound-only red + ARROWS -------
|
| 1272 |
if manual_mask_path:
|
| 1273 |
try:
|
|
|
|
| 1274 |
base_rgb = np.array(image_pil.convert("RGB"))
|
| 1275 |
base_bgr = cv2.cvtColor(base_rgb, cv2.COLOR_RGB2BGR)
|
| 1276 |
h, w = base_bgr.shape[:2]
|
| 1277 |
|
|
|
|
| 1278 |
if mask_np.shape[:2] != (h, w):
|
| 1279 |
mask_np = cv2.resize(mask_np.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST)
|
| 1280 |
|
| 1281 |
+
# If mask seems inverted (covers majority), flip it so 1 = wound
|
| 1282 |
+
wound_fraction = float(mask_np.mean())
|
| 1283 |
+
if wound_fraction > 0.5:
|
| 1284 |
+
mask_np = (1 - mask_np).astype(np.uint8)
|
| 1285 |
|
| 1286 |
+
# Output dir
|
| 1287 |
+
out_dir = os.path.dirname(roi_mask_path or result.get("saved_image_path") or manual_mask_path) or os.getcwd()
|
| 1288 |
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1289 |
|
| 1290 |
+
# Save clean binary manual mask
|
| 1291 |
manual_mask_save = os.path.join(out_dir, f"manual_mask_{ts}.png")
|
| 1292 |
cv2.imwrite(manual_mask_save, (mask_np * 255).astype(np.uint8))
|
| 1293 |
|
| 1294 |
+
# Base red overlay on wound only
|
| 1295 |
red = np.zeros_like(base_bgr); red[:] = (0, 0, 255)
|
| 1296 |
alpha = 0.55
|
| 1297 |
tinted = cv2.addWeighted(base_bgr, 1 - alpha, red, alpha, 0)
|
| 1298 |
+
mask255 = (mask_np * 255).astype(np.uint8)
|
| 1299 |
+
mask3 = cv2.merge([mask255, mask255, mask255])
|
| 1300 |
overlay = np.where(mask3 > 0, tinted, base_bgr)
|
| 1301 |
|
| 1302 |
+
# ---- Draw double-headed arrows + labels for Length & Width ----
|
| 1303 |
+
ys, xs = np.where(mask_np > 0)
|
| 1304 |
+
if xs.size and ys.size:
|
| 1305 |
+
x0, x1 = int(xs.min()), int(xs.max())
|
| 1306 |
+
y0, y1 = int(ys.min()), int(ys.max())
|
| 1307 |
+
w_px = x1 - x0 + 1
|
| 1308 |
+
h_px = y1 - y0 + 1
|
| 1309 |
|
| 1310 |
+
# Compute cm from px (fallback-safe)
|
| 1311 |
+
def _px_to_cm(px):
|
| 1312 |
+
try:
|
| 1313 |
+
return float(px) / float(px_per_cm if px_per_cm else DEFAULT_PX_PER_CM)
|
| 1314 |
+
except Exception:
|
| 1315 |
+
return float(px)
|
| 1316 |
+
|
| 1317 |
+
L_px = max(w_px, h_px)
|
| 1318 |
+
W_px = min(w_px, h_px)
|
| 1319 |
+
L_cm = _px_to_cm(L_px)
|
| 1320 |
+
W_cm = _px_to_cm(W_px)
|
| 1321 |
+
|
| 1322 |
+
# Horizontal (center y) and vertical (center x) lines
|
| 1323 |
+
cy = (y0 + y1) // 2
|
| 1324 |
+
cx = (x0 + x1) // 2
|
| 1325 |
+
h_start, h_end = (x0, cy), (x1, cy)
|
| 1326 |
+
v_start, v_end = (cx, y0), (cx, y1)
|
| 1327 |
+
|
| 1328 |
+
# Helper: outlined arrowed line (black underlay + white line)
|
| 1329 |
+
def draw_double_headed(img, p1, p2, color_fg=(255,255,255), color_bg=(0,0,0), t_fg=3, t_bg=6):
|
| 1330 |
+
cv2.arrowedLine(img, p1, p2, color_bg, t_bg, tipLength=0.03)
|
| 1331 |
+
cv2.arrowedLine(img, p2, p1, color_bg, t_bg, tipLength=0.03)
|
| 1332 |
+
cv2.arrowedLine(img, p1, p2, color_fg, t_fg, tipLength=0.03)
|
| 1333 |
+
cv2.arrowedLine(img, p2, p1, color_fg, t_fg, tipLength=0.03)
|
| 1334 |
+
|
| 1335 |
+
# Draw both arrows
|
| 1336 |
+
draw_double_headed(overlay, h_start, h_end)
|
| 1337 |
+
draw_double_headed(overlay, v_start, v_end)
|
| 1338 |
+
|
| 1339 |
+
# Helper: outlined text
|
| 1340 |
+
def put_text_outlined(img, text, org, font=cv2.FONT_HERSHEY_SIMPLEX, scale=0.7,
|
| 1341 |
+
color_fg=(255,255,255), color_bg=(0,0,0), t_fg=2, t_bg=4):
|
| 1342 |
+
cv2.putText(img, text, org, font, scale, color_bg, t_bg, cv2.LINE_AA)
|
| 1343 |
+
cv2.putText(img, text, org, font, scale, color_fg, t_fg, cv2.LINE_AA)
|
| 1344 |
+
|
| 1345 |
+
# Decide which is Length vs Width for labels
|
| 1346 |
+
if w_px >= h_px:
|
| 1347 |
+
# horizontal is length
|
| 1348 |
+
put_text_outlined(overlay, f"Length: {L_cm:.2f} cm",
|
| 1349 |
+
(x0, max(25, cy - 10)))
|
| 1350 |
+
put_text_outlined(overlay, f"Width: {W_cm:.2f} cm",
|
| 1351 |
+
(max(5, cx + 10), y0 + 25))
|
| 1352 |
+
else:
|
| 1353 |
+
# vertical is length
|
| 1354 |
+
put_text_outlined(overlay, f"Length: {L_cm:.2f} cm",
|
| 1355 |
+
(max(5, cx + 10), cy))
|
| 1356 |
+
put_text_outlined(overlay, f"Width: {W_cm:.2f} cm",
|
| 1357 |
+
(x0, max(25, cy - 10)))
|
| 1358 |
+
|
| 1359 |
+
# Save overlay with arrows
|
| 1360 |
manual_overlay_path = os.path.join(out_dir, f"segmentation_manual_{ts}.png")
|
| 1361 |
cv2.imwrite(manual_overlay_path, overlay)
|
| 1362 |
|
| 1363 |
+
# Update paths so UI shows the manual overlay (with arrows)
|
| 1364 |
visual["roi_mask_path"] = manual_mask_save
|
| 1365 |
visual["segmentation_image_path"] = manual_overlay_path
|
| 1366 |
+
visual["segmentation_roi_path"] = manual_overlay_path
|
| 1367 |
+
visual["segmentation_annotated_path"] = manual_overlay_path
|
| 1368 |
visual["segmentation_refined_type"] = "manual"
|
| 1369 |
visual["manual_mask_used"] = True
|
| 1370 |
except Exception as e:
|
| 1371 |
logging.warning(f"Failed to generate manual segmentation overlay: {e}")
|
| 1372 |
+
# ----------------------------------------------------------
|
| 1373 |
|
| 1374 |
result["visual_analysis"] = visual
|
| 1375 |
return result
|