| |
| """ |
| ============================================================================== |
| Anatomy-Aware DRR Pipeline — Phase 1 & Phase 2 Batch Processing |
| ============================================================================== |
| |
| Phase 1: Automated Table Removal (3D) |
| - Run TotalSegmentator "body" task on raw CT |
| - Replace voxels outside body mask with -1000 HU |
| - Save CT_clean.nii.gz |
| |
| Phase 2: Full 3D Anatomy Segmentation |
| - Run TotalSegmentator "total" task on CT_clean.nii.gz |
| - Produce 118-class multilabel mask.nii.gz |
| |
| Follows the directory layout specified in ANATOMY_AWARE_DRR_PIPELINE_DESIGN.md. |
| Supports resumption: already-processed cases are skipped automatically. |
| |
| Usage: |
| python run_phase1_phase2.py [--device gpu] [--fast] [--dry-run] |
| """ |
|
|
| import argparse |
| import csv |
| import datetime |
| import json |
| import logging |
| import os |
| import sys |
| import time |
| import traceback |
| from pathlib import Path |
| from typing import Dict, List, Optional, Tuple |
|
|
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| import nibabel as nib |
| import numpy as np |
| from tqdm import tqdm |
|
|
| |
| |
| |
|
|
| |
| SCRIPT_DIR = Path(__file__).resolve().parent |
| RAW_CT_DIR = SCRIPT_DIR / "data" / "lidc_nii_data_test" |
| PIPELINE_DIR = SCRIPT_DIR / "data" / "lidc_TotalSeg_test" |
| MANIFEST_DIR = PIPELINE_DIR / "manifests" |
|
|
| |
| TOTALSEG_V2_LABELS: Dict[int, str] = { |
| 1: "spleen", 2: "kidney_right", 3: "kidney_left", 4: "gallbladder", |
| 5: "liver", 6: "stomach", 7: "pancreas", 8: "adrenal_gland_right", |
| 9: "adrenal_gland_left", 10: "lung_upper_lobe_left", |
| 11: "lung_lower_lobe_left", 12: "lung_upper_lobe_right", |
| 13: "lung_middle_lobe_right", 14: "lung_lower_lobe_right", |
| 15: "esophagus", 16: "trachea", 17: "thyroid_gland", |
| 18: "small_bowel", 19: "duodenum", 20: "colon", |
| 21: "urinary_bladder", 22: "prostate", 23: "kidney_cyst_left", |
| 24: "kidney_cyst_right", 25: "sacrum", 26: "vertebrae_S1", |
| 27: "vertebrae_L5", 28: "vertebrae_L4", 29: "vertebrae_L3", |
| 30: "vertebrae_L2", 31: "vertebrae_L1", 32: "vertebrae_T12", |
| 33: "vertebrae_T11", 34: "vertebrae_T10", 35: "vertebrae_T9", |
| 36: "vertebrae_T8", 37: "vertebrae_T7", 38: "vertebrae_T6", |
| 39: "vertebrae_T5", 40: "vertebrae_T4", 41: "vertebrae_T3", |
| 42: "vertebrae_T2", 43: "vertebrae_T1", 44: "vertebrae_C7", |
| 45: "vertebrae_C6", 46: "vertebrae_C5", 47: "vertebrae_C4", |
| 48: "vertebrae_C3", 49: "vertebrae_C2", 50: "vertebrae_C1", |
| 51: "heart", 52: "aorta", 53: "pulmonary_vein", |
| 54: "brachiocephalic_trunk", 55: "subclavian_artery_right", |
| 56: "subclavian_artery_left", 57: "common_carotid_artery_right", |
| 58: "common_carotid_artery_left", 59: "brachiocephalic_vein_left", |
| 60: "brachiocephalic_vein_right", 61: "atrial_appendage_left", |
| 62: "superior_vena_cava", 63: "inferior_vena_cava", |
| 64: "portal_vein_and_splenic_vein", 65: "iliac_artery_left", |
| 66: "iliac_artery_right", 67: "iliac_vena_left", |
| 68: "iliac_vena_right", 69: "humerus_left", 70: "humerus_right", |
| 71: "scapula_left", 72: "scapula_right", 73: "clavicula_left", |
| 74: "clavicula_right", 75: "femur_left", 76: "femur_right", |
| 77: "hip_left", 78: "hip_right", 79: "spinal_cord", |
| 80: "gluteus_maximus_left", 81: "gluteus_maximus_right", |
| 82: "gluteus_medius_left", 83: "gluteus_medius_right", |
| 84: "gluteus_minimus_left", 85: "gluteus_minimus_right", |
| 86: "autochthon_left", 87: "autochthon_right", |
| 88: "iliopsoas_left", 89: "iliopsoas_right", 90: "brain", |
| 91: "skull", 92: "rib_left_1", 93: "rib_left_2", |
| 94: "rib_left_3", 95: "rib_left_4", 96: "rib_left_5", |
| 97: "rib_left_6", 98: "rib_left_7", 99: "rib_left_8", |
| 100: "rib_left_9", 101: "rib_left_10", 102: "rib_left_11", |
| 103: "rib_left_12", 104: "rib_right_1", 105: "rib_right_2", |
| 106: "rib_right_3", 107: "rib_right_4", 108: "rib_right_5", |
| 109: "rib_right_6", 110: "rib_right_7", 111: "rib_right_8", |
| 112: "rib_right_9", 113: "rib_right_10", 114: "rib_right_11", |
| 115: "rib_right_12", 116: "sternum", 117: "carpal", 118: "costal_cartilages", |
| } |
|
|
| |
| EXPECTED_THORACIC_LABELS = { |
| "lung_upper_lobe_left", "lung_lower_lobe_left", |
| "lung_upper_lobe_right", "lung_middle_lobe_right", |
| "lung_lower_lobe_right", "heart", "aorta", "trachea", |
| } |
|
|
| |
| AIR_HU = -1000 |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s [%(levelname)s] %(message)s", |
| handlers=[logging.StreamHandler(sys.stdout)], |
| ) |
| logger = logging.getLogger("pipeline") |
|
|
|
|
| |
| |
| |
|
|
| def case_dir(case_id: str) -> Path: |
| return PIPELINE_DIR / case_id |
|
|
|
|
| def phase1_paths(case_id: str) -> dict: |
| d = case_dir(case_id) / "01_body" |
| return { |
| "dir": d, |
| "body_mask": d / "body_mask.nii.gz", |
| "ct_clean": d / "CT_clean.nii.gz", |
| "metadata": d / "phase1_metadata.json", |
| } |
|
|
|
|
| def phase2_paths(case_id: str) -> dict: |
| d = case_dir(case_id) / "02_totalseg" |
| return { |
| "dir": d, |
| "segments_dir": d / "segments", |
| "mask": d / "mask.nii.gz", |
| "metadata": d / "phase2_metadata.json", |
| } |
|
|
|
|
| def qc_dir(case_id: str) -> Path: |
| return case_dir(case_id) / "qc_visuals" |
|
|
|
|
| |
| |
| |
|
|
| def run_phase1(case_id: str, raw_ct_path: Path, device: str, fast: bool) -> dict: |
| """ |
| Phase 1: Body segmentation + CT table removal. |
| |
| Returns a stats dict for metadata logging. |
| """ |
| from totalsegmentator.python_api import totalsegmentator |
|
|
| p = phase1_paths(case_id) |
| p["dir"].mkdir(parents=True, exist_ok=True) |
|
|
| |
| logger.info(f" [Phase 1] Loading raw CT: {raw_ct_path.name}") |
| ct_img = nib.load(str(raw_ct_path)) |
| ct_data = ct_img.get_fdata(dtype=np.float32) |
|
|
| |
| logger.info(f" [Phase 1] Running TotalSegmentator body task...") |
| body_seg_img = totalsegmentator( |
| input=str(raw_ct_path), |
| output=str(p["dir"] / "body_tmp"), |
| task="body", |
| ml=True, |
| device=device, |
| fast=fast, |
| quiet=True, |
| ) |
|
|
| body_data = np.asarray(body_seg_img.dataobj) |
| |
| |
| body_mask = (body_data > 0).astype(np.uint8) |
|
|
| |
| body_voxel_count = int(body_mask.sum()) |
| total_voxel_count = int(np.prod(body_mask.shape)) |
| body_fraction = body_voxel_count / total_voxel_count |
| if body_voxel_count == 0: |
| raise RuntimeError(f"Body mask is empty for {case_id}!") |
| if body_fraction < 0.01: |
| logger.warning(f" [Phase 1] Body mask occupies only {body_fraction:.4f} of volume — suspiciously small.") |
|
|
| |
| body_mask_img = nib.Nifti1Image(body_mask, ct_img.affine, ct_img.header) |
| nib.save(body_mask_img, str(p["body_mask"])) |
| logger.info(f" [Phase 1] Body mask saved. Fraction: {body_fraction:.4f}") |
|
|
| |
| ct_clean = ct_data.copy() |
| ct_clean[body_mask == 0] = AIR_HU |
|
|
| |
| assert ct_clean.shape == ct_data.shape, "Shape mismatch after cleaning!" |
|
|
| |
| ct_clean_int16 = np.clip(ct_clean, -1024, 3071).astype(np.int16) |
| ct_clean_img = nib.Nifti1Image(ct_clean_int16, ct_img.affine, ct_img.header) |
| ct_clean_img.header.set_data_dtype(np.int16) |
| nib.save(ct_clean_img, str(p["ct_clean"])) |
| logger.info(f" [Phase 1] CT_clean.nii.gz saved.") |
|
|
| |
| body_tmp = p["dir"] / "body_tmp" |
| if body_tmp.exists(): |
| import shutil |
| shutil.rmtree(body_tmp, ignore_errors=True) |
|
|
| |
| stats = { |
| "case_id": case_id, |
| "phase": 1, |
| "timestamp": datetime.datetime.now().isoformat(), |
| "raw_ct_path": str(raw_ct_path), |
| "ct_shape": list(ct_data.shape), |
| "voxel_spacing": [float(v) for v in ct_img.header.get_zooms()[:3]], |
| "body_voxel_count": body_voxel_count, |
| "body_fraction": round(body_fraction, 6), |
| "output_dtype": "int16", |
| "air_hu_value": AIR_HU, |
| } |
| with open(p["metadata"], "w") as f: |
| json.dump(stats, f, indent=2) |
|
|
| return stats |
|
|
|
|
| |
| |
| |
|
|
| def run_phase2(case_id: str, device: str, fast: bool) -> dict: |
| """ |
| Phase 2: Run full TotalSegmentator on CT_clean to produce 118-class mask. |
| |
| Returns a stats dict for metadata logging. |
| """ |
| from totalsegmentator.python_api import totalsegmentator |
|
|
| p1 = phase1_paths(case_id) |
| p2 = phase2_paths(case_id) |
| p2["dir"].mkdir(parents=True, exist_ok=True) |
|
|
| ct_clean_path = p1["ct_clean"] |
| if not ct_clean_path.exists(): |
| raise FileNotFoundError(f"CT_clean.nii.gz not found for {case_id}. Run Phase 1 first.") |
|
|
| |
| logger.info(f" [Phase 2] Running TotalSegmentator full (total) task...") |
| mask_img = totalsegmentator( |
| input=str(ct_clean_path), |
| output=str(p2["mask"]), |
| task="total", |
| ml=True, |
| device=device, |
| fast=fast, |
| quiet=True, |
| ) |
|
|
| |
| mask_data = np.asarray(mask_img.dataobj) |
| ct_clean_img = nib.load(str(ct_clean_path)) |
|
|
| |
| if mask_data.shape != ct_clean_img.shape: |
| raise RuntimeError( |
| f"Shape mismatch: mask {mask_data.shape} vs CT_clean {ct_clean_img.shape}" |
| ) |
|
|
| |
| |
| unique_labels = set(int(x) for x in np.unique(mask_data)) |
| unique_labels.discard(0) |
| valid_labels = set(TOTALSEG_V2_LABELS.keys()) |
| invalid = unique_labels - valid_labels |
| if invalid: |
| logger.warning(f" [Phase 2] Found unexpected label IDs: {invalid}") |
|
|
| |
| present_names = {TOTALSEG_V2_LABELS.get(lbl, "?") for lbl in unique_labels} |
| missing_thoracic = EXPECTED_THORACIC_LABELS - present_names |
| if missing_thoracic: |
| logger.warning(f" [Phase 2] Missing expected thoracic structures: {missing_thoracic}") |
|
|
| num_classes_found = len(unique_labels) |
| logger.info(f" [Phase 2] mask.nii.gz saved. {num_classes_found} classes found.") |
|
|
| |
| stats = { |
| "case_id": case_id, |
| "phase": 2, |
| "timestamp": datetime.datetime.now().isoformat(), |
| "mask_shape": list(mask_data.shape), |
| "num_classes_found": num_classes_found, |
| "labels_found": sorted(list(unique_labels)), |
| "label_names_found": sorted(list(present_names)), |
| "missing_thoracic": sorted(list(missing_thoracic)), |
| } |
| with open(p2["metadata"], "w") as f: |
| json.dump(stats, f, indent=2) |
|
|
| return stats |
|
|
|
|
| |
| |
| |
|
|
| def generate_qc_visuals(case_id: str, raw_ct_path: Path) -> None: |
| """ |
| Extract axial, coronal, and sagittal middle slices from: |
| 1. Raw CT |
| 2. Clean CT |
| 3. Clean CT + 3D mask overlay |
| Save as PNG images in the case's qc_visuals/ folder. |
| """ |
| p1 = phase1_paths(case_id) |
| p2 = phase2_paths(case_id) |
| out_dir = qc_dir(case_id) |
| out_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| raw_img = nib.load(str(raw_ct_path)) |
| raw_data = raw_img.get_fdata(dtype=np.float32) |
|
|
| dx, dy, dz = raw_img.header.get_zooms()[:3] |
|
|
| clean_img = nib.load(str(p1["ct_clean"])) |
| clean_data = clean_img.get_fdata(dtype=np.float32) |
|
|
| mask_img = nib.load(str(p2["mask"])) |
| mask_data = np.asarray(mask_img.dataobj).astype(np.int16) |
|
|
| shape = raw_data.shape |
| mid = (shape[0] // 2, shape[1] // 2, shape[2] // 2) |
|
|
| |
| ct_vmin, ct_vmax = -1250, 250 |
|
|
| |
| |
| |
| |
| |
| slice_specs = [ |
| ("axial", lambda vol: vol[:, :, mid[2]], dy / dx), |
| ("coronal", lambda vol: vol[:, mid[1], :], dz / dx), |
| ("sagittal", lambda vol: vol[mid[0], :, :], dz / dy), |
| ] |
|
|
| |
| for plane_name, slicer, aspect_ratio in slice_specs: |
| raw_slice = np.rot90(slicer(raw_data)) |
| clean_slice = np.rot90(slicer(clean_data)) |
| mask_slice = np.rot90(slicer(mask_data)) |
|
|
| fig, axes = plt.subplots(1, 3, figsize=(18, 6), facecolor="black") |
|
|
| |
| |
| axes[0].imshow(raw_slice, cmap="gray", vmin=ct_vmin, vmax=ct_vmax, aspect=aspect_ratio) |
| axes[0].set_title("Raw CT", color="white", fontsize=14) |
| axes[0].axis("off") |
|
|
| |
| |
| axes[1].imshow(clean_slice, cmap="gray", vmin=ct_vmin, vmax=ct_vmax, aspect=aspect_ratio) |
| axes[1].set_title("Clean CT (table removed)", color="white", fontsize=14) |
| axes[1].axis("off") |
|
|
| |
| |
| axes[2].imshow(clean_slice, cmap="gray", vmin=ct_vmin, vmax=ct_vmax, aspect=aspect_ratio) |
| mask_overlay = np.ma.masked_where(mask_slice == 0, mask_slice) |
| |
| axes[2].imshow(mask_overlay, cmap="nipy_spectral", alpha=0.45, |
| vmin=0, vmax=118, interpolation="nearest", aspect=aspect_ratio) |
| axes[2].set_title("Clean CT + Mask Overlay", color="white", fontsize=14) |
| axes[2].axis("off") |
|
|
| |
| |
| |
| |
|
|
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| fig.suptitle(f"{case_id} — {plane_name} (middle slice)", color="white", |
| fontsize=16, fontweight="bold") |
| plt.tight_layout(rect=[0, 0, 1, 0.95]) |
|
|
| out_path = out_dir / f"{plane_name}.png" |
| fig.savefig(str(out_path), dpi=120, bbox_inches="tight", |
| facecolor="black", edgecolor="none") |
| plt.close(fig) |
|
|
| logger.info(f" [QC] Saved 3 visualization PNGs to {out_dir}") |
|
|
|
|
| |
| |
| |
|
|
| def write_csv_row(csv_path: Path, row: dict, fieldnames: List[str]) -> None: |
| """Append one row to a CSV. Create with header if file does not exist.""" |
| file_exists = csv_path.exists() |
| with open(csv_path, "a", newline="") as f: |
| writer = csv.DictWriter(f, fieldnames=fieldnames) |
| if not file_exists: |
| writer.writeheader() |
| writer.writerow(row) |
|
|
|
|
| SUCCESS_FIELDS = ["case_id", "timestamp", "body_fraction", "num_classes_found", "missing_thoracic"] |
| FAILED_FIELDS = ["case_id", "timestamp", "phase_failed", "error"] |
|
|
|
|
| def save_label_dictionary() -> None: |
| """Write the canonical label dictionary JSON to manifests/.""" |
| MANIFEST_DIR.mkdir(parents=True, exist_ok=True) |
| label_path = MANIFEST_DIR / "labels_totalseg_118.json" |
| if not label_path.exists(): |
| payload = { |
| "description": "TotalSegmentator v2 'total' task — 118 anatomical classes", |
| "background_label": 0, |
| "labels": {str(k): v for k, v in TOTALSEG_V2_LABELS.items()}, |
| } |
| with open(label_path, "w") as f: |
| json.dump(payload, f, indent=2) |
| logger.info(f"Label dictionary saved: {label_path}") |
|
|
|
|
| |
| |
| |
|
|
| def is_case_complete(case_id: str) -> bool: |
| """Return True if both Phase 1 and Phase 2 outputs exist for this case.""" |
| p1 = phase1_paths(case_id) |
| p2 = phase2_paths(case_id) |
| return ( |
| p1["ct_clean"].exists() |
| and p1["body_mask"].exists() |
| and p1["metadata"].exists() |
| and p2["mask"].exists() |
| and p2["metadata"].exists() |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def discover_cases() -> List[Tuple[str, Path]]: |
| """Discover all .nii.gz files in RAW_CT_DIR and return (case_id, path) pairs.""" |
| if not RAW_CT_DIR.exists(): |
| raise FileNotFoundError(f"Raw CT directory not found: {RAW_CT_DIR}") |
|
|
| cases = [] |
| for f in sorted(RAW_CT_DIR.glob("*.nii.gz")): |
| case_id = f.name.replace(".nii.gz", "") |
| cases.append((case_id, f)) |
|
|
| if not cases: |
| raise FileNotFoundError(f"No .nii.gz files found in {RAW_CT_DIR}") |
|
|
| return cases |
|
|
|
|
| def run_pipeline(device: str, fast: bool, dry_run: bool = False) -> None: |
| """Run full Phase 1 + Phase 2 batch with resumability.""" |
| cases = discover_cases() |
| logger.info(f"Discovered {len(cases)} CT volumes in {RAW_CT_DIR}") |
|
|
| |
| MANIFEST_DIR.mkdir(parents=True, exist_ok=True) |
| save_label_dictionary() |
|
|
| |
| cases_all_path = MANIFEST_DIR / "cases_all.csv" |
| if not cases_all_path.exists(): |
| with open(cases_all_path, "w", newline="") as f: |
| writer = csv.writer(f) |
| writer.writerow(["case_id", "raw_ct_path"]) |
| for cid, cpath in cases: |
| writer.writerow([cid, str(cpath)]) |
| logger.info(f" Written {cases_all_path}") |
|
|
| success_csv = MANIFEST_DIR / "cases_success.csv" |
| failed_csv = MANIFEST_DIR / "cases_failed.csv" |
|
|
| |
| skip_count = 0 |
| to_process = [] |
| for case_id, raw_path in cases: |
| if is_case_complete(case_id): |
| skip_count += 1 |
| else: |
| to_process.append((case_id, raw_path)) |
|
|
| logger.info(f"Already complete: {skip_count} | To process: {len(to_process)}") |
|
|
| if dry_run: |
| logger.info("=== DRY RUN MODE — No processing will be performed ===") |
| logger.info(f"Would process {len(to_process)} cases.") |
| for i, (cid, rp) in enumerate(to_process[:10]): |
| p1 = phase1_paths(cid) |
| p2 = phase2_paths(cid) |
| logger.info(f" [{i+1}] {cid}") |
| logger.info(f" Input: {rp}") |
| logger.info(f" CT_clean: {p1['ct_clean']}") |
| logger.info(f" body_mask: {p1['body_mask']}") |
| logger.info(f" mask: {p2['mask']}") |
| logger.info(f" QC dir: {qc_dir(cid)}") |
| if len(to_process) > 10: |
| logger.info(f" ... and {len(to_process) - 10} more cases.") |
|
|
| |
| logger.info("") |
| logger.info("=== OUTPUT DIRECTORY STRUCTURE PREVIEW ===") |
| logger.info(f" {PIPELINE_DIR}/") |
| logger.info(f" ├── manifests/") |
| logger.info(f" │ ├── cases_all.csv") |
| logger.info(f" │ ├── cases_success.csv") |
| logger.info(f" │ ├── cases_failed.csv") |
| logger.info(f" │ └── labels_totalseg_118.json") |
| if to_process: |
| cid0 = to_process[0][0] |
| logger.info(f" ├── {cid0}/") |
| logger.info(f" │ ├── 01_body/") |
| logger.info(f" │ │ ├── body_mask.nii.gz") |
| logger.info(f" │ │ ├── CT_clean.nii.gz") |
| logger.info(f" │ │ └── phase1_metadata.json") |
| logger.info(f" │ ├── 02_totalseg/") |
| logger.info(f" │ │ ├── mask.nii.gz") |
| logger.info(f" │ │ └── phase2_metadata.json") |
| logger.info(f" │ └── qc_visuals/") |
| logger.info(f" │ ├── axial.png") |
| logger.info(f" │ ├── coronal.png") |
| logger.info(f" │ └── sagittal.png") |
| logger.info(f" └── ... ({len(to_process)} cases total)") |
| logger.info("") |
| logger.info("=== DRY RUN COMPLETE — Pass without --dry-run to execute ===") |
| return |
|
|
| |
| n_success = 0 |
| n_failed = 0 |
|
|
| for case_id, raw_path in tqdm(to_process, desc="Phase 1+2", unit="case"): |
| logger.info(f"\n{'='*60}") |
| logger.info(f"Processing: {case_id}") |
| logger.info(f"{'='*60}") |
|
|
| try: |
| t0 = time.time() |
|
|
| |
| p1_stats = run_phase1(case_id, raw_path, device=device, fast=fast) |
|
|
| |
| p2_stats = run_phase2(case_id, device=device, fast=fast) |
|
|
| |
| logger.info(f" [QC] Generating middle-slice visualizations...") |
| generate_qc_visuals(case_id, raw_path) |
|
|
| elapsed = time.time() - t0 |
| logger.info(f" DONE: {case_id} ({elapsed:.1f}s)") |
|
|
| |
| write_csv_row(success_csv, { |
| "case_id": case_id, |
| "timestamp": datetime.datetime.now().isoformat(), |
| "body_fraction": p1_stats["body_fraction"], |
| "num_classes_found": p2_stats["num_classes_found"], |
| "missing_thoracic": "|".join(p2_stats["missing_thoracic"]), |
| }, SUCCESS_FIELDS) |
| n_success += 1 |
|
|
| except Exception as e: |
| logger.error(f" FAILED: {case_id} — {e}") |
| logger.error(traceback.format_exc()) |
|
|
| |
| p1 = phase1_paths(case_id) |
| phase_failed = 1 if not p1["ct_clean"].exists() else 2 |
|
|
| write_csv_row(failed_csv, { |
| "case_id": case_id, |
| "timestamp": datetime.datetime.now().isoformat(), |
| "phase_failed": phase_failed, |
| "error": str(e), |
| }, FAILED_FIELDS) |
| n_failed += 1 |
|
|
| |
| logger.info(f"\n{'='*60}") |
| logger.info(f"BATCH COMPLETE") |
| logger.info(f" Processed: {n_success + n_failed}") |
| logger.info(f" Success: {n_success}") |
| logger.info(f" Failed: {n_failed}") |
| logger.info(f" Skipped: {skip_count} (already complete)") |
| logger.info(f" Manifests: {MANIFEST_DIR}") |
| logger.info(f"{'='*60}") |
|
|
|
|
| |
| |
| |
|
|
| def parse_args() -> argparse.Namespace: |
| parser = argparse.ArgumentParser( |
| description="Anatomy-Aware DRR Pipeline: Phase 1 (table removal) + Phase 2 (3D segmentation)" |
| ) |
| parser.add_argument( |
| "--device", type=str, default="gpu", |
| help="Device for TotalSegmentator: 'gpu', 'cpu', 'gpu:0', 'gpu:1', etc. (default: gpu)", |
| ) |
| parser.add_argument( |
| "--fast", action="store_true", |
| help="Use TotalSegmentator fast mode (3mm resolution). Faster but lower quality.", |
| ) |
| parser.add_argument( |
| "--dry-run", action="store_true", |
| help="Show what would be processed without actually running.", |
| ) |
| return parser.parse_args() |
|
|
|
|
| if __name__ == "__main__": |
| args = parse_args() |
| run_pipeline(device=args.device, fast=args.fast, dry_run=args.dry_run) |
|
|