SmartHeal commited on
Commit
e340e64
·
verified ·
1 Parent(s): 691ce33

Update src/ai_processor.py

Browse files
Files changed (1) hide show
  1. 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 the ROI mask generated by the pipeline
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 is requested and no manual mask override
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 length, width and area using the adjusted or manual mask
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
- # --- NEW: if a manual mask was supplied, create & store a manual overlay and update paths ---
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
- # Decide output directory
1292
- out_dir = os.path.dirname(roi_mask_path or result.get("saved_image_path") or manual_mask_path)
1293
- if not out_dir or not os.path.exists(out_dir):
1294
- out_dir = os.getcwd()
1295
 
 
 
1296
  ts = datetime.now().strftime("%Y%m%d_%H%M%S")
1297
 
1298
- # Save a clean binary manual mask (0/255)
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
- # Build red overlay with white contour
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
- mask3 = cv2.merge([(mask_np * 255).astype(np.uint8)] * 3)
 
1307
  overlay = np.where(mask3 > 0, tinted, base_bgr)
1308
 
1309
- cnts, _ = cv2.findContours((mask_np * 255).astype(np.uint8),
1310
- cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
1311
- if cnts:
1312
- cv2.drawContours(overlay, cnts, -1, (255, 255, 255), 2)
 
 
 
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 the UI shows the MANUAL overlay/mask
1318
  visual["roi_mask_path"] = manual_mask_save
1319
  visual["segmentation_image_path"] = manual_overlay_path
1320
- visual["segmentation_roi_path"] = manual_overlay_path # alias some UIs read
 
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