Spaces:
Sleeping
Sleeping
Update src/ai_processor.py
Browse files- src/ai_processor.py +134 -134
src/ai_processor.py
CHANGED
|
@@ -1196,142 +1196,142 @@ Automated analysis provides quantitative measurements; verify via clinical exami
|
|
| 1196 |
seg_adjust: float = 0.0,
|
| 1197 |
manual_mask_path: Optional[str] = None,
|
| 1198 |
) -> Dict:
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
|
| 1222 |
-
|
| 1223 |
-
|
| 1224 |
-
|
| 1225 |
-
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
|
| 1231 |
-
|
| 1232 |
-
try:
|
| 1233 |
-
if os.path.exists(manual_mask_path):
|
| 1234 |
-
mask_img = Image.open(manual_mask_path)
|
| 1235 |
-
else:
|
| 1236 |
-
logging.warning(f"Manual mask path does not exist: {manual_mask_path}")
|
| 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
|
| 1257 |
-
|
| 1258 |
-
mask_np = cv2.dilate((mask_np > 127).astype(np.uint8), kernel, iterations=iter_count)
|
| 1259 |
else:
|
| 1260 |
-
|
| 1261 |
-
mask_np = cv2.erode((mask_np > 127).astype(np.uint8), kernel, iterations=iter_count)
|
| 1262 |
except Exception as e:
|
| 1263 |
-
logging.warning(f"
|
| 1264 |
-
|
| 1265 |
-
#
|
| 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 |
-
|
| 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
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
|
| 1335 |
-
|
| 1336 |
-
|
| 1337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1196 |
seg_adjust: float = 0.0,
|
| 1197 |
manual_mask_path: Optional[str] = None,
|
| 1198 |
) -> Dict:
|
| 1199 |
+
"""
|
| 1200 |
+
Analyze a wound image and return a dictionary with visual analysis, report and paths.
|
| 1201 |
+
"""
|
| 1202 |
+
try:
|
| 1203 |
+
# Normalize input to PIL
|
| 1204 |
+
if isinstance(image, str):
|
| 1205 |
+
if not os.path.exists(image):
|
| 1206 |
+
raise ValueError(f"Image file not found: {image}")
|
| 1207 |
+
image_pil = Image.open(image)
|
| 1208 |
+
elif isinstance(image, Image.Image):
|
| 1209 |
+
image_pil = image
|
| 1210 |
+
elif isinstance(image, np.ndarray):
|
| 1211 |
+
image_pil = Image.fromarray(image)
|
| 1212 |
+
else:
|
| 1213 |
+
raise ValueError(f"Unsupported image type: {type(image)}")
|
| 1214 |
+
|
| 1215 |
+
# Run the standard pipeline
|
| 1216 |
+
result = self.full_analysis_pipeline(image_pil, questionnaire_data or {})
|
| 1217 |
+
|
| 1218 |
+
# If neither manual mask nor adjustment specified, return as is
|
| 1219 |
+
if (not manual_mask_path) and (abs(seg_adjust) < 1e-5):
|
| 1220 |
+
return result
|
| 1221 |
+
|
| 1222 |
+
# Extract visual analysis and calibration from 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 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):
|
| 1234 |
+
mask_img = Image.open(manual_mask_path)
|
|
|
|
| 1235 |
else:
|
| 1236 |
+
logging.warning(f"Manual mask path does not exist: {manual_mask_path}")
|
|
|
|
| 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
|
| 1328 |
+
except Exception as e:
|
| 1329 |
+
logging.error(f"Wound analysis error: {e}")
|
| 1330 |
+
return {
|
| 1331 |
+
"success": False,
|
| 1332 |
+
"error": str(e),
|
| 1333 |
+
"visual_analysis": {},
|
| 1334 |
+
"report": f"Analysis initialization failed: {str(e)}",
|
| 1335 |
+
"saved_image_path": None,
|
| 1336 |
+
"guideline_context": "",
|
| 1337 |
+
}
|