Instructions to use Lien-Feng/Lightweight-2-5D-LUNA16 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- ultralytics
How to use Lien-Feng/Lightweight-2-5D-LUNA16 with ultralytics:
# Couldn't find a valid YOLO version tag. # Replace XX with the correct version. from ultralytics import YOLOvXX model = YOLOvXX.from_pretrained("Lien-Feng/Lightweight-2-5D-LUNA16") source = 'http://images.cocodataset.org/val2017/000000039769.jpg' model.predict(source=source, save=True) - Notebooks
- Google Colab
- Kaggle
| """Assemble figures and the data appendix that the response letter cites. | |
| Reads the CSVs written by ``05_evaluate.py`` and emits: | |
| * ``figures/Figure2_FROC.{pdf,png,tiff}`` official-protocol FROC curves | |
| * ``figures/Figure4_Foldwise.*`` per-fold CPM on the official subsets | |
| * ``figures/Figure5_PairedCI.*`` paired bootstrap CPM differences | |
| * ``figures/Figure6_RSweep.*`` supervision-extent sweep | |
| * ``figures/Figure7_WminSweep.*`` minimum-box-size sweep | |
| * ``figures/Figure8_YOLO26.*`` YOLO11n vs YOLO26n FROC | |
| * ``results/RESULTS_SUMMARY.md`` every headline number, in one place | |
| Usage | |
| ----- | |
| python scripts/07_report.py | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import pandas as pd | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| from luna_rev import config as cfg | |
| from luna_rev import figures as fx | |
| R = cfg.RESULTS_DIR | |
| MAIN_ORDER = ["Exp1_2D_Loose", "Exp2_MIP_Loose", "Exp3_2p5D_Loose", "Exp4_2p5D_Strict"] | |
| def read(name: str) -> pd.DataFrame | None: | |
| p = R / name | |
| return pd.read_csv(p) if p.exists() else None | |
| def md_table(df: pd.DataFrame | None, cols=None, floatfmt=4) -> str: | |
| if df is None or df.empty: | |
| return "_(not available)_" | |
| d = df[[c for c in cols if c in df.columns]] if cols else df | |
| return d.round(floatfmt).to_markdown(index=False) | |
| def build_figures() -> list[str]: | |
| made = [] | |
| curves, main = read("froc_curves.csv"), read("table_main.csv") | |
| if curves is not None and main is not None: | |
| order = [n for n in MAIN_ORDER if n in set(curves.experiment)] | |
| fx.froc_figure(curves[curves.experiment.isin(order)], main, "Figure2_FROC", order=order) | |
| made.append("Figure2_FROC") | |
| fold = read("table_foldwise.csv") | |
| if fold is not None: | |
| fx.foldwise_figure(fold, "Figure4_Foldwise", | |
| order=[n for n in MAIN_ORDER if n in set(fold.experiment)]) | |
| made.append("Figure4_Foldwise") | |
| paired = read("table_paired.csv") | |
| if paired is not None: | |
| fx.forest_figure(paired, "Figure5_PairedCI") | |
| made.append("Figure5_PairedCI") | |
| rs = read("table_r_sweep.csv") | |
| if rs is not None: | |
| d = rs[rs.representation == "naive"].drop_duplicates("r_sample") | |
| fx.sweep_figure(d, "r_sample", "Figure6_RSweep", | |
| "Supervision-extent ratio $r_{\\mathrm{sample}}$", | |
| symbol="$r$", value_fmt="{:.1f}") | |
| made.append("Figure6_RSweep") | |
| ws = read("table_wmin_sweep.csv") | |
| if ws is not None: | |
| d = ws.drop_duplicates("w_min_px") | |
| fx.sweep_figure(d, "w_min_px", "Figure7_WminSweep", | |
| "Minimum box side $w_{\\min}$ (native pixels)", | |
| symbol="$w_{\\min}$", value_fmt="{:g} px") | |
| made.append("Figure7_WminSweep") | |
| # Drawn from curves scored on the backbone comparison's own cohort, so the | |
| # curves and the CPM values in the legend describe the same scans. | |
| y_curves, summ = read("froc_curves_yolo26.csv"), read("table_yolo26.csv") | |
| if y_curves is not None: | |
| y26 = [n for n in y_curves.experiment.unique() if n.startswith("Y26_")] | |
| if y26: | |
| order = y26 + [n for n in ("Exp3_2p5D_Loose", "Exp4_2p5D_Strict") | |
| if n in set(y_curves.experiment)] | |
| fx.froc_figure(y_curves[y_curves.experiment.isin(order)], | |
| summ if summ is not None else pd.DataFrame(columns=["experiment", "cpm"]), | |
| "Figure8_YOLO26", order=order) | |
| made.append("Figure8_YOLO26") | |
| pe = read("table_protocol_effect.csv") | |
| if pe is not None: | |
| fx.protocol_figure(pe, "Figure9_Protocol") | |
| made.append("Figure9_Protocol") | |
| return made | |
| def build_summary() -> Path: | |
| stats = json.loads((R / "dataset_stats.json").read_text(encoding="utf-8")) \ | |
| if (R / "dataset_stats.json").exists() else {} | |
| prep = json.loads((R / "prepare_report.json").read_text(encoding="utf-8")) \ | |
| if (R / "prepare_report.json").exists() else {} | |
| L: list[str] = ["# Revision R1 - results summary", ""] | |
| L += ["## Protocol verification", ""] | |
| ver = prep.get("evaluator_verification", {}) | |
| L += [f"- Evaluator reproduces the official `CADAnalysis.txt` reference exactly: " | |
| f"**{ver.get('matches_reference')}**", | |
| f"- Cohort: {prep.get('n_scans')} scans, {prep.get('n_nodules')} reference nodules, " | |
| f"{prep.get('n_excluded_findings')} irrelevant findings", | |
| "- Folds: official subset0-9; validation subset disjoint from both train and test", ""] | |
| L += ["## Training data", ""] | |
| if stats: | |
| L += [f"- Slices per representation: **{stats['n_positive_slices'] + stats['n_negative_slices']}** " | |
| f"({stats['n_positive_slices']} positive, {stats['n_negative_slices']} background)", | |
| f"- Scans contributing training slices: **{stats['n_scans']}** " | |
| f"(including {stats['n_nodule_free_scans']} nodule-free scans)", | |
| f"- Detector input: {stats['img_size']} x {stats['img_size']} (native resolution)", ""] | |
| v = pd.DataFrame(stats["variants"]) | |
| L += ["### Fraction of training boxes clamped at `w_min`", "", | |
| md_table(v, ["variant", "r_sample", "w_min_px", "n_boxes", | |
| "n_boxes_clamped_at_w_min", "frac_clamped"]), ""] | |
| sections = [ | |
| ("Main comparison (official protocol, 888 scans)", "table_main.csv", | |
| ["label", "cpm", "ci_low", "ci_high", "ci_width", "candidates_per_scan", | |
| "false_positives", "max_recall"]), | |
| ("Seven-point FROC sensitivity", "table_froc_points.csv", | |
| ["label", "fp_per_scan", "sensitivity"]), | |
| ("Paired bootstrap CPM differences", "table_paired.csv", | |
| ["contrast", "delta_cpm", "ci_low", "ci_high", "p_bootstrap", "significant"]), | |
| ("Fold-wise paired tests (official subsets)", "table_foldwise_tests.csv", | |
| ["contrast", "n_folds", "mean_diff", "median_diff", "wins_a", "wilcoxon_p", | |
| "wilcoxon_p_holm", "cohen_dz"]), | |
| ("Effect of applying annotations_excluded.csv", "table_excluded_effect.csv", | |
| ["label", "cpm_without_excluded", "cpm_with_excluded_official", "delta_cpm", | |
| "fp_without_excluded", "fp_with_excluded", "candidates_ignored", "pct_fp_removed"]), | |
| ("Supervision-extent sweep", "table_r_sweep.csv", | |
| ["label", "r_sample", "w_min_px", "cpm", "ci_low", "ci_high"]), | |
| ("Minimum-box-size sweep", "table_wmin_sweep.csv", | |
| ["label", "r_sample", "w_min_px", "cpm", "ci_low", "ci_high"]), | |
| ("Negative mining", "table_negatives.csv", | |
| ["label", "negatives", "cpm", "ci_low", "ci_high", "false_positives", | |
| "candidates_per_scan"]), | |
| ("Multi-seed repetition", "table_seeds.csv", | |
| ["configuration", "n_seeds", "cpm_mean", "cpm_sd", "cpm_range", "cpm_by_seed"]), | |
| ("YOLO11n vs YOLO26n", "table_yolo26.csv", | |
| ["label", "model", "cpm", "ci_low", "ci_high"]), | |
| ("Size-stratified recall", "table_size_recall.csv", | |
| ["label", "size_group", "n_nodules", "n_detected", "recall", | |
| "operating_point_fp_per_scan"]), | |
| ] | |
| for title, fname, cols in sections: | |
| L += [f"## {title}", "", md_table(read(fname), cols), ""] | |
| sota = read("sota_reference.csv") | |
| if sota is not None: | |
| L += ["## Published LUNA16 results, with the protocol each used", "", | |
| md_table(sota, ["method", "year", "architecture", "protocol", "metric", | |
| "value", "comparable_to_official_cpm"]), ""] | |
| prof = R / "model_profile.md" | |
| if prof.exists(): | |
| L += ["## Model and hardware profile", "", prof.read_text(encoding="utf-8"), ""] | |
| out = R / "RESULTS_SUMMARY.md" | |
| out.write_text("\n".join(L), encoding="utf-8") | |
| return out | |
| def main() -> int: | |
| made = build_figures() | |
| print("figures:", ", ".join(made) if made else "(none - run 05_evaluate.py first)") | |
| out = build_summary() | |
| print(f"summary: {out}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |