Download scripts/evaluate_sam3_lora_multiprompt_sensitivity.py from Tao-AI-Informatics/SAM3-LoRA-Breast-Lesion: direct link, hf CLI and curl.
- Browser
- Download file 5.61 kB
-
https://huggingface.co/Tao-AI-Informatics/SAM3-LoRA-Breast-Lesion/resolve/main/scripts/evaluate_sam3_lora_multiprompt_sensitivity.py
- Command line
-
hf download hf://Tao-AI-Informatics/SAM3-LoRA-Breast-Lesion/scripts/evaluate_sam3_lora_multiprompt_sensitivity.py
-
curl -L -o evaluate_sam3_lora_multiprompt_sensitivity.py https://huggingface.co/Tao-AI-Informatics/SAM3-LoRA-Breast-Lesion/resolve/main/scripts/evaluate_sam3_lora_multiprompt_sensitivity.py
5.61 kB
| #!/usr/bin/env python3 | |
| """Evaluate one SAM3 LoRA checkpoint across equivalent lesion prompts.""" | |
| from __future__ import annotations | |
| import argparse | |
| import itertools | |
| import json | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| from PIL import Image | |
| PROMPTS = ["breast lesion", "breast tumor", "breast mass", "breast nodule", "ultrasound breast lesion", "breast ultrasound mass", "hypoechoic breast lesion"] | |
| SCRIPT_DIR = Path(__file__).resolve().parent | |
| BUNDLE_ROOT = SCRIPT_DIR.parent | |
| def slug(text): | |
| return re.sub(r"[^A-Za-z0-9]+", "_", text).strip("_").lower() | |
| def args_parser(): | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--index_csv", default="data/sam3_multiprompt_lora_qc/index/test_index.csv") | |
| p.add_argument("--sam3_base_model_path", default=str(Path(os.environ.get("SAM3_CHECKPOINT", BUNDLE_ROOT / "model" / "sam3_base.pt")))) | |
| p.add_argument("--sam3_lora_path", default=str(BUNDLE_ROOT / "model" / "best_model.pt")) | |
| p.add_argument("--output_dir", default="outputs/sam3_multiprompt_lora_qc/single_prompt_lora_multiprompt_eval") | |
| p.add_argument("--prompts", nargs="+", default=PROMPTS) | |
| p.add_argument("--max_samples", type=int) | |
| p.add_argument("--overwrite", action="store_true") | |
| p.add_argument("--resume", action="store_true") | |
| return p.parse_args() | |
| def mask(path): | |
| return np.asarray(Image.open(path).convert("L")) > 0 | |
| def dice(a, b): | |
| if a.shape != b.shape: | |
| b = np.asarray(Image.fromarray(b.astype(np.uint8) * 255).resize((a.shape[1], a.shape[0]), Image.Resampling.NEAREST)) > 0 | |
| den = int(a.sum() + b.sum()) | |
| return 2 * int(np.logical_and(a, b).sum()) / den if den else 1.0 | |
| def main(): | |
| args = args_parser() | |
| out = Path(args.output_dir) | |
| out.mkdir(parents=True, exist_ok=True) | |
| lora = Path(args.sam3_lora_path) | |
| checkpoint = lora / "best_model.pt" if lora.is_dir() else lora | |
| if not checkpoint.exists(): | |
| raise FileNotFoundError(checkpoint) | |
| command = [ | |
| sys.executable, "-u", str(SCRIPT_DIR / "evaluate_sam3_test_text_prompt_segmentation.py"), | |
| "--index_csv", args.index_csv, "--sam3_model_path", args.sam3_base_model_path, | |
| "--checkpoint_path", str(checkpoint), "--output_dir", str(out), | |
| "--encoder_trainable", "lora", "--lora_rank", "8", "--lora_alpha", "16", | |
| "--prompts", *args.prompts, | |
| ] | |
| if args.max_samples: | |
| command += ["--max_samples", str(args.max_samples)] | |
| if args.overwrite: | |
| command.append("--overwrite") | |
| if args.resume: | |
| command.append("--resume") | |
| subprocess.run(command, check=True) | |
| frames = [] | |
| for prompt in args.prompts: | |
| frame = pd.read_csv(out / f"predictions_{slug(prompt)}.csv") | |
| frame["prompt"] = prompt | |
| frames.append(frame) | |
| predictions = pd.concat(frames, ignore_index=True) | |
| predictions.to_csv(out / "all_prompt_predictions.csv", index=False) | |
| per_sample = [] | |
| for sid, group in predictions.groupby("sample_id"): | |
| valid = group[group["valid_mask"].astype(bool)] | |
| masks = [(row["prompt"], mask(row["pred_mask_path"])) for _, row in valid.iterrows()] | |
| pairs = [dice(a, b) for (_, a), (_, b) in itertools.combinations(masks, 2)] | |
| areas = valid["pred_area"].astype(float).to_numpy() | |
| per_sample.append({ | |
| "sample_id": sid, | |
| "prompt_dice_std": float(valid["dice"].std(ddof=0)), | |
| "mask_area_cv": float(areas.std() / areas.mean()) if len(areas) and areas.mean() else np.nan, | |
| "pairwise_mask_dice_mean": float(np.mean(pairs)) if pairs else np.nan, | |
| "pairwise_mask_dice_min": float(np.min(pairs)) if pairs else np.nan, | |
| "valid_prompt_count": len(valid), | |
| }) | |
| sample_summary = pd.DataFrame(per_sample) | |
| sample_summary.to_csv(out / "per_sample_prompt_sensitivity.csv", index=False) | |
| metrics = pd.read_csv(out / "all_prompt_metrics.csv") | |
| best = metrics.loc[metrics["mean Dice"].idxmax()] | |
| worst = metrics.loc[metrics["mean Dice"].idxmin()] | |
| training = metrics[metrics["prompt"] == "breast lesion"].iloc[0] | |
| robustness = { | |
| "checkpoint_path": str(checkpoint.resolve()), | |
| "best_prompt_by_mean_dice": best["prompt"], | |
| "best_mean_dice": best["mean Dice"], | |
| "worst_prompt_by_mean_dice": worst["prompt"], | |
| "worst_mean_dice": worst["mean Dice"], | |
| "mean_dice_range": best["mean Dice"] - worst["mean Dice"], | |
| "std_mean_dice_across_prompts": metrics["mean Dice"].std(ddof=0), | |
| "mean_per_sample_prompt_dice_std": sample_summary["prompt_dice_std"].mean(), | |
| "mean_per_sample_area_cv": sample_summary["mask_area_cv"].mean(), | |
| "mean_pairwise_mask_dice": sample_summary["pairwise_mask_dice_mean"].mean(), | |
| "fraction_pairwise_mean_below_0p8": (sample_summary["pairwise_mask_dice_mean"] < 0.8).mean(), | |
| "max_nontraining_prompt_drop_vs_breast_lesion": training["mean Dice"] - metrics[metrics["prompt"] != "breast lesion"]["mean Dice"].min(), | |
| } | |
| robustness["large_prompt_sensitivity"] = bool( | |
| robustness["mean_dice_range"] >= 0.05 | |
| or robustness["fraction_pairwise_mean_below_0p8"] >= 0.1 | |
| or robustness["max_nontraining_prompt_drop_vs_breast_lesion"] > 0.05 | |
| ) | |
| pd.DataFrame([robustness]).to_csv(out / "prompt_sensitivity_summary.csv", index=False) | |
| with open(out / "prompt_sensitivity_summary.json", "w") as handle: | |
| json.dump(robustness, handle, indent=2) | |
| print(pd.DataFrame([robustness]).to_string(index=False)) | |
| if __name__ == "__main__": | |
| main() | |