{"cells":[{"cell_type":"markdown","id":"998b6631","metadata":{"_cell_guid":"193fd083-4485-416d-a30c-334e721e02f9","_uuid":"5fb700b5-f434-4bca-8f07-763a559c66d0","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.006206,"end_time":"2026-01-17T15:55:02.050532","exception":false,"start_time":"2026-01-17T15:55:02.044326","status":"completed"},"tags":[]},"source":"# Vesuvius Challenge - Surface Detection with nnUNet\n\n**Purpose:** 3D semantic segmentation baseline using nnUNetv2 for detecting papyrus surfaces\nin CT scan volumes from the Vesuvius Challenge.\n\n**Competition:** https://www.kaggle.com/competitions/vesuvius-challenge-surface-detection\n\n### Problem Overview\n- Input: 3D CT volumes of ancient papyrus scrolls (.tif files)\n- Output: Binary segmentation mask identifying papyrus surface pixels\n- Challenge: Partially labeled data (some regions have no ground truth)\n\n### Solution Approach\nUses nnUNet's built-in **ignore label** mechanism:\n- Label 0 = background\n- Label 1 = papyrus surface  \n- Label 2 = unlabeled/ignore (excluded from loss computation)\n\nnnUNet automatically handles partial labels using **partial losses** - only computing\ngradients on pixels with known labels (0 or 1), while ignoring label 2 regions.\n\n### Key Features of This Baseline\n1. **Native TIFF support** - Custom Tiff3DIO reader, no NIfTI conversion needed\n2. **Symlink-based data prep** - Fast setup without copying large files\n3. **Pre-processed data caching** - Upload to Kaggle dataset to skip preprocessing\n4. **Multi-GPU support** - DDP training with auto-detection\n5. **Configurable epochs** - Use built-in trainer variants (1-8000 epochs)\n6. **Visualization tools** - Training progress and prediction visualization\n\n### File Structure\n```\n/kaggle/input/vesuvius-challenge-surface-detection/\n├── train_images/*.tif      # Training CT volumes\n├── train_labels/*.tif      # Training labels (0=bg, 1=surface, 2=ignore)\n└── test_images/*.tif       # Test CT volumes (no labels)\n\n/kaggle/temp/nnUNet_data/    # Working directory (cleared between sessions)\n├── nnUNet_raw/              # Symlinks to competition data\n└── nnUNet_preprocessed/     # Preprocessed data (can cache as Kaggle dataset)\n\n/kaggle/working/             # Persistent outputs\n├── nnUNet_results/          # Trained models and checkpoints\n├── predictions_tiff/        # Inference outputs\n└── submission.zip           # Final submission\n```\n\n### Usage\n1. **Quick start:** Just run `full_pipeline()` with default settings\n2. **Fast experiments:** Use `epochs=50` or `config=\"2d\"`\n3. **Production:** Use `epochs=250-500` with `3d_fullres` or `3d_lowres`\n4. **Resume training:** Use `continue_training=True`\n\n### Performance Notes\n- Preprocessing: ~1-2 hours (cache as Kaggle dataset to skip)\n- Training: ~5-10 min/epoch on T4, ~2-3 min/epoch on better GPUs\n- fold=\"all\" validation: Runs on ALL training data after training (can be slow)\n- Inference: ~1-2 min per volume\n\n### Conversation Checkpoint\nThis code serves as a self-contained baseline. Key decisions made:\n- Using nnUNetPlannerResEncM (ResNet encoder, medium) - good speed/quality balance\n- Using 3d_fullres config - best quality single-stage segmentation\n- Using fold=\"all\" - trains on all data, no cross-validation (faster for submission)\n- Multi-GPU enabled by default via auto-detection\n- model_path auto-parses epochs/plans/config/fold from path structure\n\n---\n\n### Quick Command Reference\n\n```bash\n# Preprocessing\nnnUNetv2_plan_and_preprocess -d 100 -np 4 -pl nnUNetPlannerResEncM\n\n# Training (uses ignore label automatically)\nnnUNetv2_train 100 3d_fullres 0 -p nnUNetResEncUNetMPlans\n\n# Training with fewer epochs (faster)\nnnUNetv2_train 100 3d_fullres 0 -p nnUNetResEncUNetMPlans -tr nnUNetTrainer_250epochs\n\n# Training all folds in parallel (different GPUs)\nCUDA_VISIBLE_DEVICES=0 nnUNetv2_train 100 3d_fullres 0 ... &\nCUDA_VISIBLE_DEVICES=1 nnUNetv2_train 100 3d_fullres 1 ... &\n\n# Multi-GPU DDP training (single fold, multiple GPUs)\nnnUNetv2_train 100 3d_fullres 0 ... -num_gpus 2\n\n# Inference\nnnUNetv2_predict -d 100 -c 3d_fullres -f 0 -i INPUT -o OUTPUT -p nnUNetResEncUNetMPlans\n```\n\n### Configuration Reference\n\n#### Configurations\n| Config | Use Case | Speed | Quality |\n|--------|----------|-------|---------|\n| `2d` | Fast experiments, slice-by-slice | Fastest | Lower |\n| `3d_lowres` | Large volumes, good balance | Fast | Good |\n| `3d_fullres` | Best quality, smaller volumes | Slow | Best |\n| `3d_cascade_fullres` | Very large volumes (2-stage) | Slowest | Best |\n\n#### Planners\n| Planner | Plans Name | Notes |\n|---------|------------|-------|\n| `nnUNetPlanner` | `nnUNetPlans` | Default U-Net |\n| `nnUNetPlannerResEncM` | `nnUNetResEncUNetMPlans` | ResNet encoder, medium (recommended) |\n| `nnUNetPlannerResEncL` | `nnUNetResEncUNetLPlans` | ResNet encoder, large |\n\n#### Available Epochs (built-in trainer variants)\n```\n1, 5, 10, 20, 50, 100, 250, 500, 750, 1000, 2000, 4000, 8000\n```\n- `epochs=None` or `1000` = default full training\n- `epochs=50-100` = quick experiments\n- `epochs=250-500` = good balance for most datasets\n\n### Hyperparameter Tuning Guide\n\n**nnUNet auto-configures most parameters.** Only tune these if needed:\n\n#### What nnUNet Auto-Configures (DON'T TOUCH):\n- Patch size (based on GPU memory and data)\n- Batch size (based on GPU memory)\n- Network architecture depth\n- Normalization scheme\n- Data augmentation\n- Learning rate schedule (poly LR: `lr = initial_lr * (1 - epoch/max_epochs)^0.9`)\n\n#### What You CAN Tune:\n| Parameter | Default | How to Change | When to Change |\n|-----------|---------|---------------|----------------|\n| **Epochs** | 1000 | `epochs=250` | If converged early (check progress.png) |\n| **Planner** | nnUNetPlanner | `planner=\"nnUNetPlannerResEncM\"` | For potentially better accuracy |\n| **Folds** | 5-fold CV | `fold=\"all\"` | If limited time/compute |\n| **Config** | 3d_fullres | `config=\"2d\"` or `\"3d_lowres\"` | For faster experiments |\n| **GPUs** | auto | `num_gpus=2` | For multi-GPU training |\n\n#### Tuning Recommendations:\n1. **Start with defaults** - nnUNet is well-tuned out of the box\n2. **Try ResEnc planner** - often 1-2% better than default\n3. **Use 250-500 epochs** if model converges early (check progress.png)\n4. **Use 2d config** for quick experiments before 3d_fullres\n5. **Don't tune** batch size, patch size, LR - nnUNet handles this\n\n### References\n- [nnUNet GitHub](https://github.com/MIC-DKFZ/nnUNet)\n- [nnUNet Ignore Label Docs](https://github.com/MIC-DKFZ/nnUNet/blob/master/documentation/ignore_label.md)\n- [Vesuvius Challenge](https://scrollprize.org/)\n- Isensee et al. (2021) - nnU-Net: Nature Methods\n- Gotkowski et al. - Partial annotations paper (for ignore label support)"},{"cell_type":"markdown","id":"68a5b43e","metadata":{"_cell_guid":"18f34627-74d1-4dc9-b072-760fe5a19cec","_uuid":"25f7ae89-ae88-40df-a51a-660b3fdb8a2b","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.004779,"end_time":"2026-01-17T15:55:02.060404","exception":false,"start_time":"2026-01-17T15:55:02.055625","status":"completed"},"tags":[]},"source":"## 1. Configuration & Imports\n\n**Section Summary:**\nThis section defines all configurable parameters for the pipeline. Modify these\nto adapt the baseline to your needs.\n\n**Key Configuration Decisions:**\n- `FOLD=\"all\"`: Trains on all data without cross-validation (faster for submissions)\n- `CONFIGURATION=\"3d_lowres\"`: Good balance of speed and quality for large volumes\n- `NUM_GPUS=auto`: Uses all available GPUs for DDP training\n- `EPOCHS=None`: Default 1000 epochs (can reduce to 250-500 if converged)\n\n**Preprocessed Data Caching:**\nTo skip the 1-2 hour preprocessing step:\n1. Run preprocessing once\n2. Upload `nnUNet_preprocessed` folder as Kaggle dataset\n3. Set `PREPARED_PREPROCESSED_PATH` to that dataset path\n\n**Path Structure:**\n- INPUT_DIR: Competition data (read-only)\n- WORKING_DIR: Temporary files (/kaggle/temp, cleared between sessions)\n- OUTPUT_DIR: Persistent outputs (/kaggle/working)"},{"cell_type":"code","execution_count":2,"id":"0ab4a202","metadata":{"_cell_guid":"7e0c318c-855b-4a9f-99e0-9f43eaf31d17","_uuid":"62c06cdf-6d1d-4bd7-a0d9-42742bec8f93","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":7.301809,"end_time":"2026-01-17T15:55:09.367319","exception":false,"start_time":"2026-01-17T15:55:02.06551","status":"completed"},"tags":[]},"outputs":[],"source":"import os\nimport json\nimport shutil\nimport subprocess\nfrom functools import partial\nfrom multiprocessing import Pool\nfrom pathlib import Path\nfrom typing import Optional, Tuple, List, Literal, Union, Iterable\nimport re\n\n# =============================================================================\n# TYPE DEFINITIONS\n# =============================================================================\n\n# Available epoch counts from nnUNet's built-in trainers\n# These are pre-defined trainer classes in nnUNet - you cannot use arbitrary values\n# See: nnunetv2/training/nnUNetTrainer/variants/training_length/nnUNetTrainer_Xepochs.py\nEpochs = Literal[1, 5, 10, 20, 50, 100, 250, 500, 750, 1000, 2000, 4000, 8000]\n\n# =============================================================================\n# CONFIGURATION - MODIFY THESE FOR YOUR SETUP\n# =============================================================================\n\ndef _env_str(name: str, default: str) -> str:\n    value = os.environ.get(name)\n    return value if value is not None else default\n\n\ndef _env_int(name: str, default: Optional[int]) -> Optional[int]:\n    value = os.environ.get(name)\n    if value is None:\n        return default\n    value = value.strip().lower()\n    if value in {\"none\", \"null\", \"\"}:\n        return None\n    return int(value)\n\n\ndef _env_float(name: str, default: Optional[float]) -> Optional[float]:\n    value = os.environ.get(name)\n    if value is None:\n        return default\n    value = value.strip().lower()\n    if value in {\"none\", \"null\", \"\"}:\n        return None\n    return float(value)\n\n\ndef _env_flag(name: str, default: bool = False) -> bool:\n    value = os.environ.get(name)\n    if value is None:\n        return default\n    return value.strip().lower() not in {\"0\", \"false\", \"no\", \"off\", \"\"}\n\n\ndef _parse_fold(value: Union[str, int, None]) -> Union[str, int, None]:\n    if value is None:\n        return None\n    if isinstance(value, int):\n        return value\n    value = str(value).strip()\n    if value.isdigit():\n        return int(value)\n    return value\n\n\n# Competition data path (Kaggle dataset)\nIS_KAGGLE = bool(os.environ.get(\"KAGGLE_KERNEL_RUN_TYPE\") or os.environ.get(\"KAGGLE_URL_BASE\"))\n\nDEFAULT_INPUT_DIR = Path(\"/kaggle/input/vesuvius-challenge-surface-detection\") if IS_KAGGLE else (Path.cwd() / \"vesuvius-challenge-surface-detection\")\nINPUT_DIR = Path(os.environ.get(\"VESUVIUS_DATA_DIR\", str(DEFAULT_INPUT_DIR))).expanduser()\n\n# Pre-prepared nnUNet preprocessed dataset\n# Upload your preprocessed data as a Kaggle dataset to skip the 1-2 hour preprocessing step\n# Set to non-existent path to force fresh preprocessing\nDEFAULT_PREPARED_PREPROCESSED_PATH = Path(\"/kaggle/input/vesuvius-surface-nnunet-preprocessed\") if IS_KAGGLE else (Path.cwd() / \"vesuvius-surface-nnunet-preprocessed\")\nPREPARED_PREPROCESSED_PATH = Path(os.environ.get(\"VESUVIUS_PREPROCESSED_DIR\", str(DEFAULT_PREPARED_PREPROCESSED_PATH))).expanduser()\n\n# Working directories\nDEFAULT_WORKING_DIR = Path(\"/kaggle/temp\") if IS_KAGGLE else (Path.cwd() / \"work\")\nDEFAULT_OUTPUT_DIR = Path(\"/kaggle/working\") if IS_KAGGLE else (Path.cwd() / \"outputs\")\n\nWORKING_DIR = Path(os.environ.get(\"VESUVIUS_WORKDIR\", str(DEFAULT_WORKING_DIR))).expanduser()\nOUTPUT_DIR = Path(os.environ.get(\"VESUVIUS_OUTPUT_DIR\", str(DEFAULT_OUTPUT_DIR))).expanduser()\n\n# Use symlinks for dataset preparation (fast, low disk). Set to 0 to force copy.\nUSE_SYMLINKS = os.environ.get(\"VESUVIUS_USE_SYMLINKS\", \"1\") != \"0\"\n\n# nnUNet directory structure (follows nnUNet conventions)\nNNUNET_BASE = WORKING_DIR / \"nnUNet_data\"\nNNUNET_RAW = NNUNET_BASE / \"nnUNet_raw\"  # Small - uses symlinks to competition data\nNNUNET_PREPROCESSED = NNUNET_BASE / \"nnUNet_preprocessed\"  # Large - can use pre-prepared dataset\nNNUNET_RESULTS = OUTPUT_DIR / \"nnUNet_results\"  # Trained models go here (persisted)\n\n# Dataset configuration\nDATASET_ID = 100  # nnUNet dataset ID (arbitrary, just needs to be consistent)\nDATASET_NAME = f\"Dataset{DATASET_ID:03d}_VesuviusSurface\"\n\n# =============================================================================\n# TRAINING + INFERENCE CONFIGURATION\n# =============================================================================\n\nRUN_MODE = _env_str(\"VESUVIUS_RUN_MODE\", \"quality\").strip().lower()\nRUN_PRESETS = {\n    \"quality\": {\n        \"config\": \"3d_fullres\",\n        \"epochs\": 500,\n        \"train_fold\": \"all\",\n        \"step_size\": 0.5,\n        \"disable_tta\": False,\n        \"save_probabilities\": True,\n        \"use_best_checkpoint\": True,\n    },\n    \"balanced\": {\n        \"config\": \"3d_lowres\",\n        \"epochs\": 500,\n        \"train_fold\": \"all\",\n        \"step_size\": 0.6,\n        \"disable_tta\": False,\n        \"save_probabilities\": False,\n        \"use_best_checkpoint\": True,\n    },\n    \"fast\": {\n        \"config\": \"2d\",\n        \"epochs\": 50,\n        \"train_fold\": \"all\",\n        \"step_size\": 0.75,\n        \"disable_tta\": True,\n        \"save_probabilities\": False,\n        \"use_best_checkpoint\": False,\n    },\n}\nif RUN_MODE not in RUN_PRESETS:\n    raise ValueError(f\"VESUVIUS_RUN_MODE must be one of {sorted(RUN_PRESETS)}\")\n\n# Fold: 0-4 for 5-fold cross-validation, \"all\" for training on all data\n# - Use \"all\" for final submission (faster, uses all training data)\n# - Use 0-4 for model validation and ensembling\nFOLD: Union[int, str] = _parse_fold(_env_str(\"VESUVIUS_TRAIN_FOLD\", str(RUN_PRESETS[RUN_MODE][\"train_fold\"])))\n\n# Configuration: determines network architecture and resolution\n# - \"2d\": Fastest, processes slice-by-slice (good for quick experiments)\n# - \"3d_lowres\": Fast, handles large volumes well (NOTE: creates predicted_next_stage for cascade)\n# - \"3d_fullres\": Best quality, single-stage (RECOMMENDED for final)\n# - \"3d_cascade_fullres\": Two-stage for very large volumes (slowest, uses 3d_lowres predictions)\nCONFIGURATION = _env_str(\"VESUVIUS_CONFIG\", RUN_PRESETS[RUN_MODE][\"config\"])\n\n# Planner: determines network architecture variant\n# - \"nnUNetPlanner\": Standard U-Net encoder\n# - \"nnUNetPlannerResEncM\": ResNet encoder, medium size (RECOMMENDED - often 1-2% better)\n# - \"nnUNetPlannerResEncL\": ResNet encoder, large (more parameters, slower)\nPLANNER = _env_str(\"VESUVIUS_PLANNER\", \"nnUNetPlannerResEncM\")\nPLANS_NAME = _env_str(\"VESUVIUS_PLANS\", \"nnUNetResEncUNetMPlans\")  # Must match planner\n\n# Number of CPU workers for preprocessing and data preparation\nCPU_COUNT = os.cpu_count() or 4\nNUM_WORKERS = max(1, int(os.environ.get(\"NNUNET_NUM_WORKERS\", str(min(CPU_COUNT, 8)))))\n\n# Epochs: Number of training epochs\n# - None or 1000: Default full training\n# - 50-100: Quick experiments\n# - 250-500: Good balance (check progress.png to see if converged)\n# Must be one of: 1, 5, 10, 20, 50, 100, 250, 500, 750, 1000, 2000, 4000, 8000\nEPOCHS: Optional[Epochs] = _env_int(\"VESUVIUS_EPOCHS\", RUN_PRESETS[RUN_MODE][\"epochs\"])\n\n# Command timeout in seconds (None for no timeout)\n# Useful for Kaggle's 9-hour limit - set to e.g., 28800 (8 hours) to leave buffer\nCOMMAND_TIMEOUT: Optional[int] = _env_int(\"VESUVIUS_COMMAND_TIMEOUT\", None)\n\n# Inference controls (defaults depend on RUN_MODE)\nINFERENCE_FOLDS = os.environ.get(\"VESUVIUS_INFER_FOLDS\")\nif INFERENCE_FOLDS is not None and INFERENCE_FOLDS.strip() == \"\":\n    INFERENCE_FOLDS = None\n\nINFERENCE_STEP_SIZE = _env_float(\"VESUVIUS_STEP_SIZE\", RUN_PRESETS[RUN_MODE][\"step_size\"])\nINFERENCE_DISABLE_TTA = _env_flag(\"VESUVIUS_DISABLE_TTA\", RUN_PRESETS[RUN_MODE][\"disable_tta\"])\nINFERENCE_SAVE_PROBABILITIES = _env_flag(\"VESUVIUS_SAVE_PROBABILITIES\", RUN_PRESETS[RUN_MODE][\"save_probabilities\"])\nINFERENCE_USE_BEST_CHECKPOINT = _env_flag(\"VESUVIUS_USE_BEST_CHECKPOINT\", RUN_PRESETS[RUN_MODE][\"use_best_checkpoint\"])\nINFERENCE_CONTINUE_PREDICTION = _env_flag(\"VESUVIUS_CONTINUE_PREDICTION\", True)\nINFERENCE_NUM_PROCESSES_PREPROCESSING = _env_int(\"VESUVIUS_NPP\", None)\nINFERENCE_NUM_PROCESSES_SEGMENTATION = _env_int(\"VESUVIUS_NPS\", None)\n\n# Postprocessing controls\nFOREGROUND_LABEL = 1\nPROB_THRESHOLD = _env_float(\"VESUVIUS_PROB_THRESHOLD\", 0.75)\nMIN_COMPONENT_SIZE = _env_int(\"VESUVIUS_MIN_COMPONENT_SIZE\", 0)\nUSE_HYSTERESIS = _env_flag(\"VESUVIUS_USE_HYST\", True)\nHYST_LOW = _env_float(\"VESUVIUS_HYST_LOW\", 0.55)\nHYST_HIGH = _env_float(\"VESUVIUS_HYST_HIGH\", 0.82)\nOPENING_ITERS = _env_int(\"VESUVIUS_OPENING_ITERS\", 1)\nBORDER_ZERO = _env_int(\"VESUVIUS_BORDER_ZERO\", 3)\n\n# Default inference worker counts derived from NUM_WORKERS\nif INFERENCE_NUM_PROCESSES_PREPROCESSING is None:\n    INFERENCE_NUM_PROCESSES_PREPROCESSING = max(1, min(4, NUM_WORKERS // 2))\nif INFERENCE_NUM_PROCESSES_SEGMENTATION is None:\n    INFERENCE_NUM_PROCESSES_SEGMENTATION = max(1, min(4, NUM_WORKERS // 2))\n\n\ndef _get_gpu_count() -> int:\n    \"\"\"Get number of available CUDA GPUs.\"\"\"\n    try:\n        import torch\n        return torch.cuda.device_count() if torch.cuda.is_available() else 0\n    except ImportError:\n        return 0\n\n\nAVAILABLE_GPUS = _get_gpu_count()\nDEFAULT_DEVICE = \"cuda\" if AVAILABLE_GPUS > 0 else \"cpu\"\nTRAIN_DEVICE = _env_str(\"VESUVIUS_TRAIN_DEVICE\", DEFAULT_DEVICE)\nINFERENCE_DEVICE = _env_str(\"VESUVIUS_INFER_DEVICE\", DEFAULT_DEVICE)\n\n# Number of GPUs for DDP training\n# Auto-detected by default. Set to 1 to disable multi-GPU.\n# NOTE: Multi-GPU DDP can sometimes hang in notebook environments.\n# If training hangs, try num_gpus=1\nNUM_GPUS: int = int(os.environ.get(\"VESUVIUS_NUM_GPUS\", str(max(1, AVAILABLE_GPUS if TRAIN_DEVICE == \"cuda\" else 1))))\n\n# =============================================================================\n# PATH HELPER FUNCTIONS\n# =============================================================================\n\ndef _get_trainer_name_simple(epochs: Optional[int]) -> str:\n    \"\"\"Get trainer class name based on epochs (simple version for path construction).\"\"\"\n    if epochs is None or epochs == 1000:\n        return \"nnUNetTrainer\"\n    elif epochs == 1:\n        return \"nnUNetTrainer_1epoch\"  # Special case: singular form\n    else:\n        return f\"nnUNetTrainer_{epochs}epochs\"\n\n\ndef get_model_base_dir(\n    epochs: Optional[Epochs] = None,\n    plans: str = PLANS_NAME,\n    config: str = CONFIGURATION,\n) -> Path:\n    \"\"\"Get the model base directory (without fold_*) for the given configuration.\"\"\"\n    _epochs = epochs if epochs is not None else EPOCHS\n    trainer = _get_trainer_name_simple(_epochs)\n    return NNUNET_RESULTS / DATASET_NAME / f\"{trainer}__{plans}__{config}\"\n\n\ndef get_training_output_dir(\n    epochs: Optional[Epochs] = None,\n    plans: str = PLANS_NAME,\n    config: str = CONFIGURATION,\n    fold: Union[int, str] = FOLD\n) -> Path:\n    \"\"\"\n    Get the training output directory path based on configuration.\n    \n    nnUNet creates this folder structure:\n    NNUNET_RESULTS/Dataset100_VesuviusSurface/nnUNetTrainer__nnUNetResEncUNetMPlans__3d_lowres/fold_all/\n    \n    Use this to find checkpoints, logs, and progress.png\n    \"\"\"\n    return get_model_base_dir(epochs, plans, config) / f\"fold_{fold}\"\n\n\ndef get_progress_image_path(\n    epochs: Optional[Epochs] = None,\n    plans: str = PLANS_NAME,\n    config: str = CONFIGURATION,\n    fold: Union[int, str] = FOLD\n) -> Path:\n    \"\"\"Get path to training progress image (loss curves, metrics over epochs).\"\"\"\n    return get_training_output_dir(epochs, plans, config, fold) / \"progress.png\"\n"},{"cell_type":"markdown","id":"44658525","metadata":{"_cell_guid":"ea24161f-4aee-4d0e-bd71-40401f32b400","_uuid":"aa6d8ff2-81c6-49c2-848b-c3b156e21b72","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.004941,"end_time":"2026-01-17T15:55:09.377545","exception":false,"start_time":"2026-01-17T15:55:09.372604","status":"completed"},"tags":[]},"source":"## 2. Environment Setup\n\n**Section Summary:**\nSets up nnUNet environment variables and directory structure.\n\n**nnUNet Environment Variables:**\n- `nnUNet_raw`: Where nnUNet looks for raw dataset\n- `nnUNet_preprocessed`: Where preprocessed data is stored\n- `nnUNet_results`: Where trained models are saved\n- `nnUNet_compile`: Disable torch.compile (can cause issues)\n\n**Pre-prepared Data Handling:**\nThe `_link_prepared_preprocessed()` function handles cached preprocessed data:\n- Copies metadata files (JSON, PKL) that nnUNet may need to modify\n- Symlinks heavy data files (NPZ, B2ND) to save space\n- Handles both direct dataset paths and parent folder paths"},{"cell_type":"code","execution_count":3,"id":"06883769","metadata":{"_cell_guid":"87bff483-646f-496d-9f7d-9ee6f0de7199","_uuid":"7a1c6bbf-63b7-4dcc-8de2-58151acb3ed6","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.016602,"end_time":"2026-01-17T15:55:09.399008","exception":false,"start_time":"2026-01-17T15:55:09.382406","status":"completed"},"tags":[]},"outputs":[{"ename":"OSError","evalue":"[Errno 30] Read-only file system: '/kaggle'","output_type":"error","traceback":["\u001b[0;31m---------------------------------------------------------------------------\u001b[0m","\u001b[0;31mFileNotFoundError\u001b[0m                         Traceback (most recent call last)","File \u001b[0;32m/opt/anaconda3/lib/python3.12/pathlib.py:1311\u001b[0m, in \u001b[0;36mPath.mkdir\u001b[0;34m(self, mode, parents, exist_ok)\u001b[0m\n\u001b[1;32m   1310\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 1311\u001b[0m     os\u001b[38;5;241m.\u001b[39mmkdir(\u001b[38;5;28mself\u001b[39m, mode)\n\u001b[1;32m   1312\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mFileNotFoundError\u001b[39;00m:\n","\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/kaggle/temp/nnUNet_data/nnUNet_raw'","\nDuring handling of the above exception, another exception occurred:\n","\u001b[0;31mFileNotFoundError\u001b[0m                         Traceback (most recent call last)","File \u001b[0;32m/opt/anaconda3/lib/python3.12/pathlib.py:1311\u001b[0m, in \u001b[0;36mPath.mkdir\u001b[0;34m(self, mode, parents, exist_ok)\u001b[0m\n\u001b[1;32m   1310\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 1311\u001b[0m     os\u001b[38;5;241m.\u001b[39mmkdir(\u001b[38;5;28mself\u001b[39m, mode)\n\u001b[1;32m   1312\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mFileNotFoundError\u001b[39;00m:\n","\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/kaggle/temp/nnUNet_data'","\nDuring handling of the above exception, another exception occurred:\n","\u001b[0;31mFileNotFoundError\u001b[0m                         Traceback (most recent call last)","File \u001b[0;32m/opt/anaconda3/lib/python3.12/pathlib.py:1311\u001b[0m, in \u001b[0;36mPath.mkdir\u001b[0;34m(self, mode, parents, exist_ok)\u001b[0m\n\u001b[1;32m   1310\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 1311\u001b[0m     os\u001b[38;5;241m.\u001b[39mmkdir(\u001b[38;5;28mself\u001b[39m, mode)\n\u001b[1;32m   1312\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mFileNotFoundError\u001b[39;00m:\n","\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/kaggle/temp'","\nDuring handling of the above exception, another exception occurred:\n","\u001b[0;31mOSError\u001b[0m                                   Traceback (most recent call last)","Cell \u001b[0;32mIn[3], line 91\u001b[0m\n\u001b[1;32m     87\u001b[0m     \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mLocation: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtarget_dir\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m     88\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[0;32m---> 91\u001b[0m setup_environment()\n","Cell \u001b[0;32mIn[3], line 4\u001b[0m, in \u001b[0;36msetup_environment\u001b[0;34m()\u001b[0m\n\u001b[1;32m      2\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Set up nnUNet environment variables and directories.\"\"\"\u001b[39;00m\n\u001b[1;32m      3\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m d \u001b[38;5;129;01min\u001b[39;00m [NNUNET_RAW, NNUNET_PREPROCESSED, NNUNET_RESULTS, OUTPUT_DIR]:\n\u001b[0;32m----> 4\u001b[0m     d\u001b[38;5;241m.\u001b[39mmkdir(parents\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m, exist_ok\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m      6\u001b[0m os\u001b[38;5;241m.\u001b[39menviron[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnnUNet_raw\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m(NNUNET_RAW)\n\u001b[1;32m      7\u001b[0m os\u001b[38;5;241m.\u001b[39menviron[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnnUNet_preprocessed\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m(NNUNET_PREPROCESSED)\n","File \u001b[0;32m/opt/anaconda3/lib/python3.12/pathlib.py:1315\u001b[0m, in \u001b[0;36mPath.mkdir\u001b[0;34m(self, mode, parents, exist_ok)\u001b[0m\n\u001b[1;32m   1313\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m parents \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mparent \u001b[38;5;241m==\u001b[39m \u001b[38;5;28mself\u001b[39m:\n\u001b[1;32m   1314\u001b[0m         \u001b[38;5;28;01mraise\u001b[39;00m\n\u001b[0;32m-> 1315\u001b[0m     \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mparent\u001b[38;5;241m.\u001b[39mmkdir(parents\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m, exist_ok\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m   1316\u001b[0m     \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmkdir(mode, parents\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m, exist_ok\u001b[38;5;241m=\u001b[39mexist_ok)\n\u001b[1;32m   1317\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mOSError\u001b[39;00m:\n\u001b[1;32m   1318\u001b[0m     \u001b[38;5;66;03m# Cannot rely on checking for EEXIST, since the operating system\u001b[39;00m\n\u001b[1;32m   1319\u001b[0m     \u001b[38;5;66;03m# could give priority to other errors like EACCES or EROFS\u001b[39;00m\n","File \u001b[0;32m/opt/anaconda3/lib/python3.12/pathlib.py:1315\u001b[0m, in \u001b[0;36mPath.mkdir\u001b[0;34m(self, mode, parents, exist_ok)\u001b[0m\n\u001b[1;32m   1313\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m parents \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mparent \u001b[38;5;241m==\u001b[39m \u001b[38;5;28mself\u001b[39m:\n\u001b[1;32m   1314\u001b[0m         \u001b[38;5;28;01mraise\u001b[39;00m\n\u001b[0;32m-> 1315\u001b[0m     \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mparent\u001b[38;5;241m.\u001b[39mmkdir(parents\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m, exist_ok\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m   1316\u001b[0m     \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmkdir(mode, parents\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m, exist_ok\u001b[38;5;241m=\u001b[39mexist_ok)\n\u001b[1;32m   1317\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mOSError\u001b[39;00m:\n\u001b[1;32m   1318\u001b[0m     \u001b[38;5;66;03m# Cannot rely on checking for EEXIST, since the operating system\u001b[39;00m\n\u001b[1;32m   1319\u001b[0m     \u001b[38;5;66;03m# could give priority to other errors like EACCES or EROFS\u001b[39;00m\n","File \u001b[0;32m/opt/anaconda3/lib/python3.12/pathlib.py:1315\u001b[0m, in \u001b[0;36mPath.mkdir\u001b[0;34m(self, mode, parents, exist_ok)\u001b[0m\n\u001b[1;32m   1313\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m parents \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mparent \u001b[38;5;241m==\u001b[39m \u001b[38;5;28mself\u001b[39m:\n\u001b[1;32m   1314\u001b[0m         \u001b[38;5;28;01mraise\u001b[39;00m\n\u001b[0;32m-> 1315\u001b[0m     \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mparent\u001b[38;5;241m.\u001b[39mmkdir(parents\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m, exist_ok\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m   1316\u001b[0m     \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmkdir(mode, parents\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m, exist_ok\u001b[38;5;241m=\u001b[39mexist_ok)\n\u001b[1;32m   1317\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mOSError\u001b[39;00m:\n\u001b[1;32m   1318\u001b[0m     \u001b[38;5;66;03m# Cannot rely on checking for EEXIST, since the operating system\u001b[39;00m\n\u001b[1;32m   1319\u001b[0m     \u001b[38;5;66;03m# could give priority to other errors like EACCES or EROFS\u001b[39;00m\n","File \u001b[0;32m/opt/anaconda3/lib/python3.12/pathlib.py:1311\u001b[0m, in \u001b[0;36mPath.mkdir\u001b[0;34m(self, mode, parents, exist_ok)\u001b[0m\n\u001b[1;32m   1307\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m   1308\u001b[0m \u001b[38;5;124;03mCreate a new directory at this given path.\u001b[39;00m\n\u001b[1;32m   1309\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m   1310\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 1311\u001b[0m     os\u001b[38;5;241m.\u001b[39mmkdir(\u001b[38;5;28mself\u001b[39m, mode)\n\u001b[1;32m   1312\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mFileNotFoundError\u001b[39;00m:\n\u001b[1;32m   1313\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m parents \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mparent \u001b[38;5;241m==\u001b[39m \u001b[38;5;28mself\u001b[39m:\n","\u001b[0;31mOSError\u001b[0m: [Errno 30] Read-only file system: '/kaggle'"]}],"source":"def setup_environment():\n    \"\"\"Set up nnUNet environment variables and directories.\"\"\"\n    WORKING_DIR.mkdir(parents=True, exist_ok=True)\n    for d in [NNUNET_RAW, NNUNET_PREPROCESSED, NNUNET_RESULTS, OUTPUT_DIR]:\n        d.mkdir(parents=True, exist_ok=True)\n    \n    os.environ[\"nnUNet_raw\"] = str(NNUNET_RAW)\n    os.environ[\"nnUNet_preprocessed\"] = str(NNUNET_PREPROCESSED)\n    os.environ[\"nnUNet_results\"] = str(NNUNET_RESULTS)\n    os.environ[\"nnUNet_compile\"] = \"true\"\n    \n    print(f\"INPUT_DIR: {INPUT_DIR}\")\n    print(f\"WORKING_DIR: {WORKING_DIR}\")\n    print(f\"OUTPUT_DIR: {OUTPUT_DIR}\")\n    print(f\"nnUNet_raw: {NNUNET_RAW}\")\n    print(f\"nnUNet_preprocessed: {NNUNET_PREPROCESSED}\")\n    print(f\"nnUNet_results: {NNUNET_RESULTS}\")\n    print(f\"RUN_MODE: {RUN_MODE}\")\n    print(f\"TRAIN config: {CONFIGURATION}, fold={FOLD}, epochs={EPOCHS or 1000}\")\n    print(f\"TRAIN_DEVICE: {TRAIN_DEVICE}, NUM_GPUS: {NUM_GPUS}\")\n    print(f\"INFER folds: {INFERENCE_FOLDS or 'auto'}\")\n    print(f\"INFER step_size: {INFERENCE_STEP_SIZE}, disable_tta: {INFERENCE_DISABLE_TTA}\")\n    print(f\"INFER save_probabilities: {INFERENCE_SAVE_PROBABILITIES}\")\n    print(f\"INFER use_best_checkpoint: {INFERENCE_USE_BEST_CHECKPOINT}\")\n    print(f\"POST prob_threshold: {PROB_THRESHOLD}, min_component_size: {MIN_COMPONENT_SIZE}\")\n    print(f\"USE_SYMLINKS: {USE_SYMLINKS}\")\n    print(f\"nnUNet_USE_BLOSC2: {os.environ.get('nnUNet_USE_BLOSC2', 'not set')} (0=NPZ, 1=blosc2)\")\n    print(f\"NUM_WORKERS: {NUM_WORKERS}\")\n    print(f\"INFERENCE_NPP: {INFERENCE_NUM_PROCESSES_PREPROCESSING}\")\n    print(f\"INFERENCE_NPS: {INFERENCE_NUM_PROCESSES_SEGMENTATION}\")\n\n\ndef _link_prepared_preprocessed() -> bool:\n    \"\"\"\n    Link/copy pre-prepared preprocessed data if available.\n    \n    Copies the folder structure and metadata files (which nnUNet may need to modify),\n    but symlinks the heavy .npz/.b2nd data files to save space.\n    \n    Handles two possible structures:\n    1. PREPARED_PREPROCESSED_PATH points directly to Dataset100_* folder\n    2. PREPARED_PREPROCESSED_PATH contains Dataset100_* as subfolder\n    \n    Returns True if linked/copied successfully.\n    \"\"\"\n    if not PREPARED_PREPROCESSED_PATH.exists():\n        return False\n    \n    # Determine source directory - could be the path itself or a subfolder\n    source_dir = PREPARED_PREPROCESSED_PATH\n    if not (source_dir / \"dataset.json\").exists():\n        # Look for Dataset folder inside\n        dataset_folders = list(PREPARED_PREPROCESSED_PATH.glob(f\"Dataset*_{DATASET_NAME.split('_')[1]}*\"))\n        if not dataset_folders:\n            dataset_folders = list(PREPARED_PREPROCESSED_PATH.glob(\"Dataset*\"))\n        if dataset_folders:\n            source_dir = dataset_folders[0]\n        else:\n            print(f\"No dataset folder found in {PREPARED_PREPROCESSED_PATH}\")\n            return False\n    \n    target_dir = NNUNET_PREPROCESSED / DATASET_NAME\n    \n    if target_dir.exists():\n        print(f\"Preprocessed data already exists: {target_dir}\")\n        return True\n    \n    print(f\"Linking preprocessed data from: {source_dir}\")\n    target_dir.mkdir(parents=True, exist_ok=True)\n    \n    # Files that nnUNet may need to write/modify - copy these\n    copy_patterns = ['*.json', '*.pkl', '*.txt']\n    \n    # Heavy data files - symlink these  \n    symlink_patterns = ['*.npz', '*.npy', '*.b2nd']\n    \n    copied = 0\n    linked = 0\n    \n    for src_path in source_dir.rglob('*'):\n        if src_path.is_dir():\n            continue\n            \n        # Compute relative path and create target path\n        rel_path = src_path.relative_to(source_dir)\n        dst_path = target_dir / rel_path\n        dst_path.parent.mkdir(parents=True, exist_ok=True)\n        \n        # Check if this is a heavy data file (symlink) or metadata (copy)\n        is_data_file = any(src_path.match(pat) for pat in symlink_patterns)\n        \n        if is_data_file:\n            if not dst_path.exists():\n                dst_path.symlink_to(src_path.resolve())\n                linked += 1\n        else:\n            if not dst_path.exists():\n                shutil.copy2(src_path, dst_path)\n                copied += 1\n    \n    print(f\"Prepared preprocessed data: {copied} files copied, {linked} files symlinked\")\n    print(f\"Location: {target_dir}\")\n    return True\n\n\nsetup_environment()\n"},{"cell_type":"markdown","id":"63575b99","metadata":{"_cell_guid":"6a77e8bd-fee1-4c9f-9b5e-b21af0f52acf","_uuid":"665ab694-31f8-43dd-8780-5e98a42c1bf5","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.004814,"end_time":"2026-01-17T15:55:09.408817","exception":false,"start_time":"2026-01-17T15:55:09.404003","status":"completed"},"tags":[]},"source":"## 3. Installation & Imports\n\n**Section Summary:**\nInstalls required packages and imports libraries.\n\n**Key Dependencies:**\n- `nnunetv2`: The nnUNet framework (includes PyTorch)\n- `nibabel`: For loading NIfTI files (nnUNet's default format)\n- `tifffile`: For reading/writing TIFF files (competition format)\n- `matplotlib`: For visualization\n\n**Important Notes:**\n- Set `nnUNet_USE_BLOSC2=1` BEFORE importing nnunetv2 to use blosc2 format (faster)\n- Blosc2 format can cause issues on some systems"},{"cell_type":"code","execution_count":null,"id":"08de2e69","metadata":{"_cell_guid":"69b7c4ca-d393-41d0-ae77-e96a9e952a13","_uuid":"82a5abad-98d7-4e54-91bf-78afe62ddbb7","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":79.101412,"end_time":"2026-01-17T15:56:28.515035","exception":false,"start_time":"2026-01-17T15:55:09.413623","status":"completed"},"tags":[]},"outputs":[],"source":"import sys\n\nif IS_KAGGLE:\n    subprocess.run([\"mkdir\", \"-p\", \"/kaggle/temp\"], check=False)\n    subprocess.run([\"mkdir\", \"-p\", \"predictions_tiff\"], check=False)\n    subprocess.run([\n        sys.executable, \"-m\", \"pip\", \"install\",\n        \"nnunetv2\", \"nibabel\", \"tifffile\", \"tqdm\",\n        \"-q\", \"--no-index\", \"-f\", \"/kaggle/input/surface-packages-offline\"\n    ], check=False)\nelse:\n    Path(\"predictions_tiff\").mkdir(parents=True, exist_ok=True)\n\n# IMPORTANT: Set this BEFORE importing nnunetv2\n# Blosc2 is a newer compression format but can cause compatibility issues\nos.environ[\"nnUNet_USE_BLOSC2\"] = \"1\"  # Use blosc2 format (faster, smaller files)\n\nimport nibabel as nib\nimport numpy as np\nfrom scipy import ndimage\nimport pandas as pd\nimport tifffile\nimport matplotlib.pyplot as plt\nfrom tqdm.auto import tqdm\n\n# Show GPU configuration\nprint(f\"Available GPUs: {_get_gpu_count()}\")\nprint(f\"Using NUM_GPUS={NUM_GPUS}\")"},{"cell_type":"markdown","id":"bf46a921","metadata":{"_cell_guid":"a7785be3-ccdb-4015-83dd-3fe2b43bb7eb","_uuid":"4d2688e3-0654-46d7-9325-87be0c939e27","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.005061,"end_time":"2026-01-17T15:56:28.52548","exception":false,"start_time":"2026-01-17T15:56:28.520419","status":"completed"},"tags":[]},"source":"## 4. Data Utilities\n\n**Section Summary:**\nHelper functions for handling TIFF and NIfTI file formats.\n\n**TIFF Format:**\n- Competition uses 3D TIFF files for CT volumes\n- nnUNet natively supports TIFF via our custom Tiff3DIO reader\n- No conversion to NIfTI needed (saves disk space and time)\n\n**Spacing Information:**\n- nnUNet requires voxel spacing (physical size of each voxel)\n- We use isotropic spacing (1.0, 1.0, 1.0) as competition doesn't specify\n- Stored in JSON sidecar files alongside TIFF images"},{"cell_type":"code","execution_count":null,"id":"2dc606e7","metadata":{"_cell_guid":"d1c4bcf6-c084-4149-83ee-e5dce0c64f6c","_uuid":"96f7afc7-cd4d-4304-93b4-dcb7fd1b096b","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.013358,"end_time":"2026-01-17T15:56:28.544299","exception":false,"start_time":"2026-01-17T15:56:28.530941","status":"completed"},"tags":[]},"outputs":[],"source":"def create_spacing_json(output_path: Path, shape: tuple, spacing: tuple = (1.0, 1.0, 1.0)):\n    \"\"\"Create JSON sidecar with spacing info for TIFF files.\"\"\"\n    json_data = {\"spacing\": list(spacing)}\n    with open(output_path, \"w\") as f:\n        json.dump(json_data, f)\n\n\ndef load_nifti(path: Path) -> np.ndarray:\n    \"\"\"Load NIfTI file (used for loading nnUNet predictions).\"\"\"\n    return nib.load(str(path)).get_fdata()\n\n\ndef create_dataset_json(output_dir: Path, num_training: int, file_ending: str = \".tif\") -> dict:\n    \"\"\"Create dataset.json with ignore label support and 3D TIFF reader.\"\"\"\n    \n    dataset_json = {\n        \"channel_names\": {\"0\": \"CT\"},\n        \"labels\": {\"background\": 0, \"surface\": 1, \"ignore\": 2},\n        \"numTraining\": num_training,\n        \"file_ending\": file_ending,\n        \"overwrite_image_reader_writer\": \"Tiff3DIO\"\n    }\n    \n    json_path = output_dir / \"dataset.json\"\n    with open(json_path, \"w\") as f:\n        json.dump(dataset_json, f, indent=4)\n    \n    print(f\"Created {json_path}\")\n    print(f\"  - {num_training} training cases\")\n    print(\"  - Labels: background(0), surface(1), ignore(2)\")\n    print(\"  - Reader: Tiff3DIO (3D TIFF)\")\n    \n    return dataset_json\n"},{"cell_type":"markdown","id":"6faec3e2","metadata":{"_cell_guid":"9cd77ab4-9f29-4203-8193-2b29d9013f0f","_uuid":"9ece55f8-b8fc-457a-9563-95b732f60c1e","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.004957,"end_time":"2026-01-17T15:56:28.554187","exception":false,"start_time":"2026-01-17T15:56:28.54923","status":"completed"},"tags":[]},"source":"## 5. Dataset Preparation\n\n**Section Summary:**\nConverts competition data to nnUNet's expected format.\n\n**nnUNet Dataset Structure:**\n```\nnnUNet_raw/Dataset100_VesuviusSurface/\n├── imagesTr/           # Training images\n│   ├── case001_0000.tif  # _0000 suffix = channel 0 (only one for CT)\n│   └── case001.json # Spacing information\n├── labelsTr/           # Training labels\n│   ├── case001.tif\n│   └── case001.json\n└── dataset.json        # Dataset configuration\n```\n\n**Symlink Strategy:**\n- Uses symlinks instead of copying (fast, saves disk space)\n- Only JSON sidecar files are created (spacing info)\n- Original TIFF files remain in competition dataset\n\n**Parallel Processing:**\n- Uses multiprocessing for faster preparation\n- ~2 minutes for 806 training cases"},{"cell_type":"code","execution_count":null,"id":"e142acfa","metadata":{"_cell_guid":"0b404165-d4c2-4292-8193-99db616a0e9c","_uuid":"fad5a235-b611-49cd-b8f2-5519de775dd7","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.016703,"end_time":"2026-01-17T15:56:28.575809","exception":false,"start_time":"2026-01-17T15:56:28.559106","status":"completed"},"tags":[]},"outputs":[],"source":"def _sidecar_path_for(dest_path: Path) -> Path:\n    \"\"\"Return the spacing sidecar path, stripping _0000 if present.\"\"\"\n    stem = dest_path.stem\n    if stem.endswith(\"_0000\"):\n        stem = stem[:-5]\n    return dest_path.with_name(stem + \".json\")\n\n\ndef prepare_single_case(\n    src_path: Path,\n    dest_path: Path,\n    json_path: Optional[Path] = None,\n    use_symlinks: bool = True\n) -> bool:\n    \"\"\"\n    Prepare a single TIFF file for nnUNet: create symlink/copy and JSON sidecar.\n    Returns True on success, False on failure.\n    \"\"\"\n    try:\n        # Get shape for JSON\n        with tifffile.TiffFile(src_path) as tif:\n            shape = tif.pages[0].shape if len(tif.pages) == 1 else (len(tif.pages), *tif.pages[0].shape)\n        \n        # Link or copy file\n        if use_symlinks:\n            if not dest_path.exists():\n                try:\n                    dest_path.symlink_to(src_path.resolve())\n                except OSError:\n                    shutil.copy2(src_path, dest_path)\n        else:\n            shutil.copy2(src_path, dest_path)\n        \n        # Create JSON sidecar\n        if json_path is None:\n            json_path = _sidecar_path_for(dest_path)\n        if not json_path.exists():\n            create_spacing_json(json_path, shape)\n        return True\n        \n    except Exception as e:\n        print(f\"Error processing {src_path.name}: {e}\")\n        return False\n\n\ndef _prepare_training_case(\n    img_path: Path,\n    train_labels_dir: Path,\n    images_dir: Path,\n    labels_dir: Path,\n    use_symlinks: bool\n) -> bool:\n    \"\"\"Worker function for parallel dataset preparation.\"\"\"\n    case_id = img_path.stem\n    label_path = train_labels_dir / img_path.name\n    \n    if not label_path.exists():\n        return False\n    \n    img_ok = prepare_single_case(\n        img_path,\n        images_dir / f\"{case_id}_0000.tif\",\n        images_dir / f\"{case_id}.json\",\n        use_symlinks\n    )\n    \n    label_ok = prepare_single_case(\n        label_path,\n        labels_dir / f\"{case_id}.tif\",\n        labels_dir / f\"{case_id}.json\",\n        use_symlinks\n    )\n    \n    return img_ok and label_ok\n\n\ndef prepare_dataset(input_dir: Path, max_cases: Optional[int] = None, use_symlinks: Optional[bool] = None):\n    \"\"\"\n    Convert competition data to nnUNet format using TIFF directly (no NIfTI).\n    Uses multiprocessing for faster preparation.\n    \n    Competition structure:\n    - train_images/*.tif  (3D volumes)\n    - train_labels/*.tif  (3D labels: 0=bg, 1=surface, 2=ignore)\n    \"\"\"\n    dataset_dir = NNUNET_RAW / DATASET_NAME\n    images_dir = dataset_dir / \"imagesTr\"\n    labels_dir = dataset_dir / \"labelsTr\"\n    \n    images_dir.mkdir(parents=True, exist_ok=True)\n    labels_dir.mkdir(parents=True, exist_ok=True)\n    \n    train_images_dir = input_dir / \"train_images\"\n    train_labels_dir = input_dir / \"train_labels\"\n    \n    if not train_images_dir.exists():\n        print(f\"ERROR: {train_images_dir} not found!\")\n        return None\n    if not train_labels_dir.exists():\n        print(f\"ERROR: {train_labels_dir} not found!\")\n        return None\n\n    if use_symlinks is None:\n        use_symlinks = USE_SYMLINKS\n    \n    image_files = sorted(train_images_dir.glob(\"*.tif\"))\n    if max_cases:\n        image_files = image_files[:max_cases]\n    \n    print(f\"Found {len(image_files)} training cases\")\n    print(f\"Using {'symlinks' if use_symlinks else 'copy'}\")\n    print(f\"Processing with {NUM_WORKERS} workers...\")\n    \n    # Create worker function with fixed arguments\n    worker = partial(\n        _prepare_training_case,\n        train_labels_dir=train_labels_dir,\n        images_dir=images_dir,\n        labels_dir=labels_dir,\n        use_symlinks=use_symlinks\n    )\n    \n    # Process in parallel with progress bar\n    if NUM_WORKERS <= 1:\n        results = [worker(p) for p in tqdm(image_files, total=len(image_files), desc=\"Preparing dataset\")]\n    else:\n        try:\n            with Pool(NUM_WORKERS) as pool:\n                results = list(tqdm(\n                    pool.imap(worker, image_files),\n                    total=len(image_files),\n                    desc=\"Preparing dataset\"\n                ))\n        except Exception as exc:\n            print(f\"WARNING: multiprocessing failed ({exc}); falling back to single process\")\n            results = [worker(p) for p in tqdm(image_files, total=len(image_files), desc=\"Preparing dataset\")]\n    \n    num_converted = sum(results)\n    create_dataset_json(dataset_dir, num_converted, file_ending=\".tif\")\n    \n    print(f\"\\nDataset prepared: {num_converted} cases\")\n    print(f\"Location: {dataset_dir}\")\n    \n    return dataset_dir\n"},{"cell_type":"markdown","id":"e32d5fe6","metadata":{"_cell_guid":"72871a71-ad2f-431d-ad48-fbdc93ce0874","_uuid":"9d20116b-546f-4bd6-8b08-8c3b1f8488af","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.004874,"end_time":"2026-01-17T15:56:28.585678","exception":false,"start_time":"2026-01-17T15:56:28.580804","status":"completed"},"tags":[]},"source":"## 6. Synthetic Data (for testing)\n\n**Section Summary:**\nCreates small synthetic datasets for testing the pipeline without real data.\n\n**Use Cases:**\n- Testing pipeline setup before using real data\n- Debugging data loading issues\n- Quick iteration on code changes\n\n**Generated Data:**\n- Small 3D volumes (64x64x64 by default)\n- Spherical shell as \"surface\" (label 1)\n- Random ignore regions (label 2)\n- Random background noise"},{"cell_type":"code","execution_count":null,"id":"309d78b7","metadata":{"_cell_guid":"7fc46883-0414-437d-b4d0-058394c93cec","_uuid":"ae7d78c3-e0a1-4259-9ec0-56147aca59e0","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.013876,"end_time":"2026-01-17T15:56:28.604367","exception":false,"start_time":"2026-01-17T15:56:28.590491","status":"completed"},"tags":[]},"outputs":[],"source":"def create_single_synthetic_case(\n    case_id: str,\n    size: Tuple[int, int, int],\n    images_dir: Path,\n    labels_dir: Path\n):\n    \"\"\"Create a single synthetic training case.\"\"\"\n    # Create volume with structure\n    volume = np.random.randn(*size).astype(np.float32) * 0.1\n    \n    # Add spherical shell as \"surface\"\n    z, y, x = np.ogrid[:size[0], :size[1], :size[2]]\n    center = np.array(size) // 2\n    dist = np.sqrt((z - center[0])**2 + (y - center[1])**2 + (x - center[2])**2)\n    shell = (dist > 15) & (dist < 20)\n    volume[shell] += 1.0\n    \n    # Create labels\n    labels = np.zeros(size, dtype=np.uint8)\n    labels[shell] = 1  # Surface\n    \n    # Add ignore regions (label 2)\n    ignore_mask = np.random.random(size) < 0.15\n    labels[ignore_mask] = 2\n    \n    # Save as TIFF\n    tifffile.imwrite(images_dir / f\"{case_id}_0000.tif\", volume)\n    tifffile.imwrite(labels_dir / f\"{case_id}.tif\", labels)\n    \n    # Create JSON sidecars\n    create_spacing_json(images_dir / f\"{case_id}.json\", size)\n    create_spacing_json(labels_dir / f\"{case_id}.json\", size)\n\n\ndef create_synthetic_dataset(num_cases: int = 5, size: Tuple[int, int, int] = (64, 64, 64)):\n    \"\"\"Create synthetic 3D data with ignore regions for testing (TIFF format).\"\"\"\n    \n    dataset_dir = NNUNET_RAW / DATASET_NAME\n    images_dir = dataset_dir / \"imagesTr\"\n    labels_dir = dataset_dir / \"labelsTr\"\n    \n    images_dir.mkdir(parents=True, exist_ok=True)\n    labels_dir.mkdir(parents=True, exist_ok=True)\n    \n    for i in tqdm(range(num_cases), desc=\"Creating synthetic data\"):\n        case_id = f\"case_{i:03d}\"\n        create_single_synthetic_case(case_id, size, images_dir, labels_dir)\n    \n    create_dataset_json(dataset_dir, num_cases, file_ending=\".tif\")\n    print(f\"Created synthetic dataset: {dataset_dir}\")\n    \n    return dataset_dir\n\n# To use synthetic data instead of real data, uncomment:\n# create_synthetic_dataset(num_cases=5)"},{"cell_type":"markdown","id":"26419a24","metadata":{"_cell_guid":"cda8a2f7-aa28-427e-8600-6eb9cddfe4fd","_uuid":"68b6c269-e1c1-4956-9403-a23160df8c23","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.004891,"end_time":"2026-01-17T15:56:28.614186","exception":false,"start_time":"2026-01-17T15:56:28.609295","status":"completed"},"tags":[]},"source":"## 7. nnUNet Commands\n\n**Section Summary:**\nWrapper functions for nnUNet command-line tools.\n\n**nnUNet Pipeline Steps:**\n1. `run_preprocessing()` → `nnUNetv2_plan_and_preprocess`\n   - Analyzes dataset statistics\n   - Creates experiment plans (network architecture, patch size, etc.)\n   - Preprocesses data (resampling, normalization)\n   - Time: 1-2 hours for full dataset\n\n2. `run_training()` → `nnUNetv2_train`\n   - Trains the network\n   - Saves checkpoints every 50 epochs\n   - Generates progress.png with loss curves\n   - Time: 5-10 min/epoch on T4\n\n3. `run_inference()` → `nnUNetv2_predict`\n   - Runs trained model on test data\n   - Outputs NIfTI predictions (converted to TIFF later)\n   - Time: 1-2 min per volume\n\n**Important Training Notes:**\n- `fold=\"all\"`: After training, nnUNet validates on ALL training data\n  This can be slow (50+ min) but cannot be skipped with built-in flags\n- Multi-GPU: Use `-num_gpus X` for DDP training\n- Epochs: Use `-tr nnUNetTrainer_Xepochs` for custom epoch counts"},{"cell_type":"code","execution_count":null,"id":"a10b5cb6","metadata":{"_cell_guid":"f7372f1e-dcc8-4bee-8320-037609ecd707","_uuid":"6c762a1b-4033-46bd-8518-7245c917e103","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.019441,"end_time":"2026-01-17T15:56:28.638679","exception":false,"start_time":"2026-01-17T15:56:28.619238","status":"completed"},"tags":[]},"outputs":[],"source":"def _run_command(\n    cmd: str,\n    name: str = \"Command\",\n    tail_lines: int = 20,\n    timeout: Optional[int] = COMMAND_TIMEOUT\n) -> bool:\n    \"\"\"\n    Execute shell command and handle output parsing.\n    \n    Args:\n        cmd: Shell command to execute\n        name: Display name for logging\n        tail_lines: Number of stdout lines to show on success\n        timeout: Timeout in seconds (None for no timeout)\n    \n    Returns:\n        True if command succeeded, False otherwise\n    \"\"\"\n    print(f\"Running: {cmd}\")\n    if timeout:\n        print(f\"Timeout: {timeout}s ({timeout/3600:.1f}h)\")\n    \n    try:\n        result = subprocess.run(\n            cmd,\n            shell=True,\n            capture_output=True,\n            text=True,\n            timeout=timeout\n        )\n    except subprocess.TimeoutExpired:\n        print(f\"{name} TIMEOUT after {timeout}s!\")\n        return False\n    \n    if result.returncode != 0:\n        print(f\"{name} FAILED!\")\n        print(f\"STDERR:\\n{result.stderr[-3000:]}\")\n        return False\n    \n    print(f\"{name} complete!\")\n    if result.stdout.strip():\n        lines = result.stdout.strip().split('\\n')\n        print('\\n'.join(lines[-tail_lines:]))\n    \n    return True\n\n\ndef run_preprocessing(\n    dataset_id: int = DATASET_ID,\n    planner: str = PLANNER,\n    num_workers: int = NUM_WORKERS,\n    configurations: Optional[List[str]] = None,\n    timeout: Optional[int] = COMMAND_TIMEOUT\n) -> bool:\n    \"\"\"\n    Run nnUNet preprocessing.\n    \n    Args:\n        dataset_id: nnUNet dataset ID\n        planner: Planner class name\n        num_workers: Number of CPU workers for parallel processing\n        configurations: List of configs to preprocess (e.g., [\"3d_fullres\"])\n        timeout: Timeout in seconds (None for no timeout)\n    \n    Returns:\n        True if preprocessing succeeded\n    \"\"\"\n    if configurations is None:\n        configurations = [CONFIGURATION]\n    \n    cmd = f\"nnUNetv2_plan_and_preprocess -d {dataset_id:03d} -np {num_workers}\"\n    cmd += f\" -pl {planner}\"\n    cmd += f\" -c {' '.join(configurations)}\"\n    \n    return _run_command(cmd, \"Preprocessing\", timeout=timeout)\n\n\ndef _get_trainer_name(epochs: Optional[Epochs]) -> str:\n    \"\"\"Get trainer class name based on epochs.\"\"\"\n    if epochs is None or epochs == 1000:\n        return \"nnUNetTrainer\"\n    elif epochs == 1:\n        return \"nnUNetTrainer_1epoch\"\n    else:\n        return f\"nnUNetTrainer_{epochs}epochs\"\n\n\ndef run_training(\n    dataset_id: int = DATASET_ID,\n    config: str = CONFIGURATION,\n    fold: Union[int, str] = FOLD,\n    plans: str = PLANS_NAME,\n    epochs: Optional[Epochs] = EPOCHS,\n    pretrained_weights: Optional[Path] = None,\n    continue_training: bool = False,\n    only_run_validation: bool = False,\n    disable_checkpointing: bool = False,\n    npz: bool = False,\n    num_gpus: int = NUM_GPUS,\n    device: str = TRAIN_DEVICE,\n    timeout: Optional[int] = COMMAND_TIMEOUT\n) -> bool:\n    \"\"\"\n    Run nnUNet training.\n    \n    Args:\n        dataset_id: nnUNet dataset ID\n        config: Configuration name (3d_fullres, 2d, etc.)\n        fold: Fold number (0-4) or \"all\" for training on all data\n        plans: Plans name matching the planner used\n        epochs: Number of epochs. Available: 1, 5, 10, 20, 50, 100, 150, 200, 250, \n                300, 400, 500, 750, 1000, 2000, 4000, 8000. None = 1000 (default)\n        pretrained_weights: Optional path to pretrained checkpoint for fine-tuning\n        continue_training: Continue from last checkpoint (add -c flag)\n        only_run_validation: Only run validation, skip training\n        disable_checkpointing: Disable saving checkpoints (saves disk space)\n        npz: Save softmax outputs during validation (needed for ensembling)\n        num_gpus: Number of GPUs for DDP training (default: auto-detected)\n        device: \"cuda\", \"cpu\", or \"mps\"\n        timeout: Timeout in seconds (None for no timeout)\n    \n    Returns:\n        True if training succeeded\n    \n    Note:\n        For multi-GPU (DDP) training, batch size should be divisible by num_gpus.\n        The first run extracts preprocessed data - wait for GPU usage before \n        starting additional folds on other GPUs.\n        \n        When using fold=\"all\", nnUNet will run validation on ALL training data\n        after training completes, which can be slow. This is normal behavior.\n    \"\"\"\n    trainer = _get_trainer_name(epochs)\n    cmd = f\"nnUNetv2_train {dataset_id:03d} {config} {fold} -p {plans} -tr {trainer}\"\n    \n    if pretrained_weights:\n        cmd += f\" -pretrained_weights {pretrained_weights}\"\n    if continue_training:\n        # Find checkpoint to resume from: prefer checkpoint_final.pth, fallback to checkpoint_best.pth\n        model_dir = get_training_output_dir(epochs=epochs, plans=plans, config=config, fold=fold)\n        checkpoint_final = model_dir / \"checkpoint_final.pth\"\n        checkpoint_best = model_dir / \"checkpoint_best.pth\"\n        if checkpoint_final.exists():\n            print(f\"Resuming from: {checkpoint_final}\")\n            cmd += \" --c\"\n        elif checkpoint_best.exists():\n            print(f\"Resuming from: {checkpoint_best}\")\n            cmd += \" --c\"\n        else:\n            print(f\"WARNING: No checkpoint found in {model_dir}, starting fresh\")\n    if only_run_validation:\n        cmd += \" --val\"\n    if disable_checkpointing:\n        cmd += \" --disable_checkpointing\"\n    if npz:\n        cmd += \" --npz\"\n    if num_gpus > 1:\n        cmd += f\" -num_gpus {num_gpus}\"\n    if device:\n        cmd += f\" -device {device}\"\n    \n    epochs_str = epochs if epochs else 1000\n    gpu_str = f\", {num_gpus} GPUs\" if num_gpus > 1 else \"\"\n    return _run_command(cmd, f\"Training ({epochs_str} epochs{gpu_str})\", tail_lines=30, timeout=timeout)\n\n\ndef _normalize_folds(folds: Optional[Union[int, str, Iterable[int]]]) -> Optional[List[str]]:\n    if folds is None:\n        return None\n    if isinstance(folds, (list, tuple, set)):\n        return [str(f) for f in folds]\n    if isinstance(folds, int):\n        return [str(folds)]\n    folds = str(folds).strip()\n    if folds == \"\" or folds.lower() == \"auto\":\n        return None\n    if folds.lower() == \"all\":\n        return [\"all\"]\n    parts = [p for p in re.split(r\"[,\\s]+\", folds) if p]\n    return parts or None\n\n\ndef _detect_available_folds(model_base_dir: Path, checkpoint_name: str) -> Optional[List[str]]:\n    fold_all = model_base_dir / \"fold_all\" / checkpoint_name\n    if fold_all.exists():\n        return [\"all\"]\n    folds: List[str] = []\n    for i in range(5):\n        if (model_base_dir / f\"fold_{i}\" / checkpoint_name).exists():\n            folds.append(str(i))\n    return folds or None\n\n\ndef _select_checkpoint_name(model_base_dir: Path, folds: List[str], prefer_best: bool) -> str:\n    if not prefer_best:\n        return \"checkpoint_final.pth\"\n    for fold in folds:\n        fold_dir = model_base_dir / (f\"fold_{fold}\" if fold != \"all\" else \"fold_all\")\n        if not (fold_dir / \"checkpoint_best.pth\").exists():\n            return \"checkpoint_final.pth\"\n    return \"checkpoint_best.pth\"\n\n\ndef run_inference(\n    input_dir: Path,\n    output_dir: Path,\n    dataset_id: int = DATASET_ID,\n    config: str = CONFIGURATION,\n    fold: Optional[Union[int, str, Iterable[int]]] = INFERENCE_FOLDS,\n    plans: str = PLANS_NAME,\n    epochs: Optional[Epochs] = EPOCHS,\n    save_probabilities: bool = INFERENCE_SAVE_PROBABILITIES,\n    step_size: Optional[float] = INFERENCE_STEP_SIZE,\n    disable_tta: bool = INFERENCE_DISABLE_TTA,\n    use_best_checkpoint: bool = INFERENCE_USE_BEST_CHECKPOINT,\n    checkpoint_name: Optional[str] = None,\n    continue_prediction: bool = INFERENCE_CONTINUE_PREDICTION,\n    device: str = INFERENCE_DEVICE,\n    num_processes_preprocessing: int = INFERENCE_NUM_PROCESSES_PREPROCESSING,\n    num_processes_segmentation: int = INFERENCE_NUM_PROCESSES_SEGMENTATION,\n    timeout: Optional[int] = COMMAND_TIMEOUT\n) -> bool:\n    \"\"\"\n    Run inference with trained model.\n    \n    Args:\n        input_dir: Directory with test images (must have _0000 suffix)\n        output_dir: Directory to save predictions\n        dataset_id: nnUNet dataset ID\n        config: Configuration name\n        fold: Fold(s) used for training. None/\"auto\" = auto-detect. \"all\" uses fold_all.\n        plans: Plans name\n        epochs: Epochs used during training (must match trained model)\n        save_probabilities: Whether to save probability maps (.npz files)\n        step_size: Sliding window step size (0 < step_size <= 1). Larger is faster, less accurate.\n        disable_tta: Disable mirroring TTA for faster inference\n        use_best_checkpoint: Prefer checkpoint_best.pth if it exists for all folds\n        checkpoint_name: Explicit checkpoint name to use (overrides use_best_checkpoint)\n        continue_prediction: Skip cases that already have outputs\n        device: \"cuda\", \"cpu\", or \"mps\"\n        num_processes_preprocessing: Parallel processes for preprocessing\n        num_processes_segmentation: Parallel processes for segmentation\n        timeout: Timeout in seconds (None for no timeout)\n    \n    Returns:\n        True if inference succeeded\n    \"\"\"\n    output_dir.mkdir(parents=True, exist_ok=True)\n    \n    trainer = _get_trainer_name(epochs)\n    model_base_dir = get_model_base_dir(epochs=epochs, plans=plans, config=config)\n\n    fold_list = _normalize_folds(fold)\n    if fold_list is None:\n        fold_list = _detect_available_folds(model_base_dir, \"checkpoint_best.pth\")\n        if fold_list is None:\n            fold_list = _detect_available_folds(model_base_dir, \"checkpoint_final.pth\")\n    if fold_list is None:\n        fold_list = [\"all\"]\n\n    if checkpoint_name is None:\n        checkpoint_name = _select_checkpoint_name(model_base_dir, fold_list, use_best_checkpoint)\n\n    if step_size is not None and not (0 < step_size <= 1):\n        raise ValueError(f\"step_size must be in (0, 1], got {step_size}\")\n\n    cmd = f\"nnUNetv2_predict -d {dataset_id:03d} -c {config}\"\n    cmd += f\" -f {' '.join(fold_list)}\"\n    cmd += f\" -i {input_dir} -o {output_dir} -p {plans} -tr {trainer}\"\n    cmd += f\" -npp {num_processes_preprocessing} -nps {num_processes_segmentation}\"\n    if step_size is not None:\n        cmd += f\" -step_size {step_size}\"\n    if checkpoint_name:\n        cmd += f\" -chk {checkpoint_name}\"\n    if device:\n        cmd += f\" -device {device}\"\n    if disable_tta:\n        cmd += \" --disable_tta\"\n    if continue_prediction:\n        cmd += \" --continue_prediction\"\n    cmd += \" --verbose\"\n    \n    if save_probabilities:\n        cmd += \" --save_probabilities\"\n    \n    return _run_command(cmd, \"Inference\", timeout=timeout)\n"},{"cell_type":"markdown","id":"0b009335","metadata":{"_cell_guid":"8d468a99-805e-476f-a555-8490b119ea0d","_uuid":"0f38a3b7-0208-4b1f-9d16-96d6a61c1dc0","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.005076,"end_time":"2026-01-17T15:56:28.648721","exception":false,"start_time":"2026-01-17T15:56:28.643645","status":"completed"},"tags":[]},"source":"## 8. Test Data Preparation\n\n**Section Summary:**\nPrepares test data for nnUNet inference.\n\n**Required Format:**\n- Files must have `_0000` suffix (channel indicator)\n- JSON sidecar files with spacing info\n\n**Note:**\nTest images are in competition data, we just need to rename/link them\nto match nnUNet's expected naming convention."},{"cell_type":"code","execution_count":null,"id":"e266fd6b","metadata":{"_cell_guid":"f489ef92-b1b0-4dbd-b99c-9a074a90c9e7","_uuid":"a25f68d5-bb4c-42cc-b6af-777552aa5be2","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.01122,"end_time":"2026-01-17T15:56:28.664901","exception":false,"start_time":"2026-01-17T15:56:28.653681","status":"completed"},"tags":[]},"outputs":[],"source":"def prepare_test_data(input_dir: Path, output_dir: Path, use_symlinks: Optional[bool] = None) -> Path:\n    \"\"\"Prepare test TIFF images for nnUNet inference.\"\"\"\n    \n    output_dir.mkdir(parents=True, exist_ok=True)\n    \n    test_images_dir = input_dir / \"test_images\"\n    \n    if not test_images_dir.exists():\n        print(f\"ERROR: {test_images_dir} not found!\")\n        return output_dir\n\n    if use_symlinks is None:\n        use_symlinks = USE_SYMLINKS\n    \n    test_files = sorted(test_images_dir.glob(\"*.tif\"))\n    print(f\"Found {len(test_files)} test cases\")\n    print(f\"Using {'symlinks' if use_symlinks else 'copy'}\")\n    \n    for img_path in tqdm(test_files, desc=\"Preparing test data\"):\n        case_id = img_path.stem\n        prepare_single_case(\n            img_path,\n            output_dir / f\"{case_id}_0000.tif\",\n            output_dir / f\"{case_id}.json\",\n            use_symlinks\n        )\n    \n    return output_dir\n"},{"cell_type":"markdown","id":"7ede70f5","metadata":{"_cell_guid":"e36848ed-dfb4-4d7f-9137-3a878257ad6d","_uuid":"9c9ffb9e-d7a5-4b75-9d24-8120da4aa8a6","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.004808,"end_time":"2026-01-17T15:56:28.674703","exception":false,"start_time":"2026-01-17T15:56:28.669895","status":"completed"},"tags":[]},"source":"## 9. Inference Utilities\n\n**Section Summary:**\nFunctions for converting nnUNet predictions to submission format.\n\n**nnUNet Output Formats:**\nWhen using our Tiff3DIO reader with `save_probabilities=True`:\n- `.npz` - Probability maps (preferred for conversion)\n- `.tif` - Direct predictions (already in our format)\n- `.pkl` - Metadata\n- `.json` - Configuration info\n\n**Conversion Process:**\n1. Load probability maps from .npz files\n2. Apply argmax to get class predictions\n3. Save as uint8 TIFF (0=background, 1=surface)"},{"cell_type":"code","execution_count":null,"id":"aad163c7","metadata":{"_cell_guid":"77f89718-b429-4287-bcdb-c2977dc93fb4","_uuid":"d5edfb02-bf4f-4782-8c10-9b6a535f9def","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.012866,"end_time":"2026-01-17T15:56:28.692405","exception":false,"start_time":"2026-01-17T15:56:28.679539","status":"completed"},"tags":[]},"outputs":[],"source":"def load_probabilities(npz_path: Path) -> np.ndarray:\n    \"\"\"\n    Load probability maps from nnUNet inference.\n    \n    Only available if inference was run with save_probabilities=True.\n    Shape: (num_classes, D, H, W) with float32 values in [0, 1].\n    \"\"\"\n    data = np.load(npz_path)\n    return data['probabilities']\n\n\ndef _postprocess_binary_mask(mask: np.ndarray, min_component_size: int = 0) -> np.ndarray:\n    \"\"\"Remove tiny components to reduce false positives (optional).\"\"\"\n    if min_component_size is None or min_component_size <= 0:\n        return mask\n    try:\n        import scipy.ndimage as ndi\n    except ImportError:\n        print(\"WARNING: scipy not available; skipping postprocessing\")\n        return mask\n\n    structure = np.ones((3, 3, 3), dtype=np.uint8)\n    labeled, num = ndi.label(mask, structure=structure)\n    if num == 0:\n        return mask\n    counts = np.bincount(labeled.ravel())\n    remove = counts < min_component_size\n    remove[0] = False\n    mask_out = mask.copy()\n    mask_out[remove[labeled]] = 0\n    return mask_out\n\n\ndef _to_binary_mask(pred: np.ndarray, foreground_label: int) -> np.ndarray:\n    if pred.max() <= 1:\n        return (pred > 0).astype(np.uint8)\n    return (pred == foreground_label).astype(np.uint8)\n\n\n\ndef _topology_postprocess(mask: np.ndarray,\n                          opening_iters: int = 0,\n                          border_zero: int = 0,\n                          min_component_size: int = 0) -> np.ndarray:\n    \"\"\"Topology-friendly postprocessing for binary masks.\n\n    - binary_opening cuts thin bridges (reduces merges)\n    - border_zero removes edge artifacts\n    - min_component_size removes tiny islands (optional; use carefully)\n    \"\"\"\n    m = (mask > 0)\n    if border_zero and border_zero > 0:\n        bz = int(border_zero)\n        if m.shape[0] > 2*bz:\n            m[:bz, :, :] = False\n            m[-bz:, :, :] = False\n        if m.shape[1] > 2*bz:\n            m[:, :bz, :] = False\n            m[:, -bz:, :] = False\n        if m.shape[2] > 2*bz:\n            m[:, :, :bz] = False\n            m[:, :, -bz:] = False\n\n    if opening_iters and opening_iters > 0:\n        # 3D connectivity-1 structure (6-neighborhood); safer than full 26-neighborhood for cutting thin bridges\n        structure = ndimage.generate_binary_structure(3, 1)\n        m = ndimage.binary_opening(m, structure=structure, iterations=int(opening_iters))\n\n    out = m.astype(np.uint8)\n\n    if min_component_size and min_component_size > 0:\n        out = _postprocess_binary_mask(out, int(min_component_size))\n\n    return out\n\n\ndef _hysteresis_from_prob(prob_fg: np.ndarray, low: float, high: float) -> np.ndarray:\n    \"\"\"Hysteresis thresholding via binary propagation (fast & topology-friendly).\"\"\"\n    if not (0 <= low <= 1 and 0 <= high <= 1 and low <= high):\n        raise ValueError(f\"Invalid hysteresis thresholds low={low}, high={high}\")\n    seeds = prob_fg >= high\n    mask = prob_fg >= low\n    if not seeds.any():\n        return (prob_fg >= high).astype(np.uint8)\n    rec = ndimage.binary_propagation(seeds, mask=mask)\n    return rec.astype(np.uint8)\n\n\ndef predictions_to_tiff(\n    pred_dir: Path,\n    output_dir: Path,\n    prob_threshold: Optional[float] = PROB_THRESHOLD,\n    foreground_label: int = FOREGROUND_LABEL,\n    min_component_size: int = MIN_COMPONENT_SIZE,\n    use_hysteresis: bool = USE_HYSTERESIS,\n    hyst_low: float = HYST_LOW,\n    hyst_high: float = HYST_HIGH,\n    opening_iters: int = OPENING_ITERS,\n    border_zero: int = BORDER_ZERO,\n    overwrite: bool = True,\n):\n    \"\"\"\n    Convert nnUNet predictions to 3D TIFF files.\n    \n    nnUNet outputs:\n    - .npz files with probability maps (if save_probabilities=True)\n    - .tif files with predictions (Tiff3DIO format)\n    - .pkl files with metadata\n    \n    This function:\n    1. First tries to load .npz files and convert probabilities to binary predictions\n    2. Falls back to .tif files if .npz not found\n    3. Saves as uint8 TIFF (0=background, 1=surface)\n    \"\"\"\n    output_dir.mkdir(parents=True, exist_ok=True)\n    \n    # Try NPZ files first (probability maps)\n    npz_files = list(pred_dir.glob(\"*.npz\"))\n    tif_files = list(pred_dir.glob(\"*.tif\"))\n    nii_files = list(pred_dir.glob(\"*.nii.gz\"))\n    \n    if npz_files:\n        print(f\"Converting {len(npz_files)} NPZ probability files to TIFF...\")\n        for npz_path in tqdm(npz_files, desc=\"Converting to TIFF\"):\n            case_id = npz_path.stem\n            out_path = output_dir / f\"{case_id}.tif\"\n            if out_path.exists() and not overwrite:\n                continue\n            probs = load_probabilities(npz_path)\n            if probs.ndim == 4 and foreground_label < probs.shape[0]:\n                prob_fg = probs[foreground_label]\n                if use_hysteresis and hyst_low is not None and hyst_high is not None:\n                    pred = _hysteresis_from_prob(prob_fg, float(hyst_low), float(hyst_high))\n                elif prob_threshold is not None:\n                    pred = (prob_fg >= float(prob_threshold)).astype(np.uint8)\n                else:\n                    pred = np.argmax(probs, axis=0).astype(np.uint8)\n                    pred = _to_binary_mask(pred, foreground_label)\n            else:\n                pred = np.argmax(probs, axis=0).astype(np.uint8)\n                pred = _to_binary_mask(pred, foreground_label)\n\n            pred = _topology_postprocess(pred,\n                                        opening_iters=opening_iters,\n                                        border_zero=border_zero,\n                                        min_component_size=min_component_size)\n            tifffile.imwrite(out_path, pred)\n    elif tif_files:\n        print(f\"Copying {len(tif_files)} TIFF prediction files...\")\n        for tif_path in tqdm(tif_files, desc=\"Copying TIFF\"):\n            case_id = tif_path.stem\n            out_path = output_dir / f\"{case_id}.tif\"\n            if out_path.exists() and not overwrite:\n                continue\n            pred = tifffile.imread(str(tif_path)).astype(np.uint8)\n            pred = _to_binary_mask(pred, foreground_label)\n            pred = _topology_postprocess(pred, opening_iters=opening_iters, border_zero=border_zero, min_component_size=min_component_size)\n            tifffile.imwrite(out_path, pred)\n    # Try NIfTI as last resort (legacy format)\n    elif nii_files:\n        print(f\"Converting {len(nii_files)} NIfTI files to TIFF...\")\n        for nii_path in tqdm(nii_files, desc=\"Converting to TIFF\"):\n            case_id = nii_path.stem.replace(\".nii\", \"\")\n            out_path = output_dir / f\"{case_id}.tif\"\n            if out_path.exists() and not overwrite:\n                continue\n            pred = load_nifti(nii_path).astype(np.uint8)\n            pred = _to_binary_mask(pred, foreground_label)\n            pred = _topology_postprocess(pred, opening_iters=opening_iters, border_zero=border_zero, min_component_size=min_component_size)\n            tifffile.imwrite(out_path, pred)\n    else:\n        print(f\"WARNING: No prediction files found in {pred_dir}\")\n        print(\"  Checked for: *.npz, *.tif, *.nii.gz\")\n"},{"cell_type":"markdown","id":"eb0bd4ae","metadata":{"_cell_guid":"cbcac86d-dfc8-4dc2-b7de-a00ddf092c58","_uuid":"5462cd2d-43da-4208-aaea-d978d66ff6e8","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.004794,"end_time":"2026-01-17T15:56:28.702561","exception":false,"start_time":"2026-01-17T15:56:28.697767","status":"completed"},"tags":[]},"source":"## 10. Full Pipeline\n\n**Section Summary:**\nMain entry point that orchestrates the entire nnUNet pipeline.\n\n**Pipeline Steps:**\n1. Environment setup (create directories, set env vars)\n2. Raw data preparation (symlinks + JSON sidecars)\n3. Preprocessing (or link pre-prepared data)\n4. Training\n5. Inference + TIFF conversion\n6. Visualization (progress + predictions)\n\n**Usage Patterns:**\n```python\n# Full pipeline with defaults\nfull_pipeline()\n\n# Quick experiment\nfull_pipeline(epochs=50, config=\"2d\")\n\n# Resume training\nfull_pipeline(continue_training=True, epochs=250)\n\n# Inference only (trained model exists)\nfull_pipeline(do_preprocess=False, do_train=False)\n```\n\n**Important Notes:**\n- When `fold=\"all\"`, validation runs on ALL training data after training\n- This validation step can take 50+ minutes but cannot be skipped\n- Progress image and predictions are auto-displayed after completion"},{"cell_type":"code","execution_count":null,"id":"55ecad67","metadata":{"papermill":{"duration":0.012917,"end_time":"2026-01-17T15:56:28.720359","exception":false,"start_time":"2026-01-17T15:56:28.707442","status":"completed"},"tags":[]},"outputs":[],"source":"def _parse_model_path(model_path: Union[str, Path, None]) -> Tuple[Optional[Path], Optional[int], Optional[str], Optional[str], Optional[str]]:\n    \"\"\"\n    Parse model path to extract configuration parameters.\n    \n    Model path format: .../DatasetXXX_Name/TrainerName__PlansName__Config/fold_X/checkpoint.pth\n    \n    Returns:\n        (model_dir, epochs, plans, config, fold) - extracted from path, None if not found\n    \n    Note: Parses path structure even if file doesn't exist (for path validation).\n    \"\"\"\n    if model_path is None:\n        return None, None, None, None, None\n    \n    model_path = Path(model_path)\n    \n    # Determine model directory from path (even if doesn't exist)\n    # If path ends with .pth, use parent directory\n    if model_path.suffix == \".pth\" or (model_path.exists() and model_path.is_file()):\n        model_dir = model_path.parent\n    else:\n        model_dir = model_path\n    \n    # Try to parse folder structure\n    # Expected: .../fold_X or .../TrainerName__PlansName__Config/fold_X\n    try:\n        parts = model_dir.parts\n        \n        # Find fold\n        fold = None\n        for part in reversed(parts):\n            if part.startswith(\"fold_\"):\n                fold = part.replace(\"fold_\", \"\")\n                break\n        \n        # Find trainer__plans__config\n        epochs = None\n        plans = None\n        config = None\n        for part in parts:\n            if \"__\" not in part:\n                continue\n            segments = part.split(\"__\")\n            if len(segments) >= 3:\n                trainer_name = segments[0]\n                plans = segments[1]\n                config = segments[2]\n                # Extract epochs from trainer name\n                if \"epochs\" in trainer_name:\n                    import re\n                    match = re.search(r'(\\d+)epochs?', trainer_name)\n                    if match:\n                        epochs = int(match.group(1))\n                elif trainer_name == \"nnUNetTrainer\":\n                    epochs = 1000  # Default\n                break\n        \n        return model_dir, epochs, plans, config, fold\n    except Exception:\n        return model_dir, None, None, None, None"},{"cell_type":"code","execution_count":null,"id":"96ead3ef","metadata":{"_cell_guid":"42cebe6a-6001-4cad-9755-3d79e31e51ad","_uuid":"9079d68c-f6f7-4363-9090-849178090320","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.01992,"end_time":"2026-01-17T15:56:28.745252","exception":false,"start_time":"2026-01-17T15:56:28.725332","status":"completed"},"tags":[]},"outputs":[],"source":"def full_pipeline(\n    # Data options\n    max_cases: Optional[int] = None,\n    # Stage control\n    do_preprocess: bool = True,\n    do_train: bool = True,\n    do_inference: bool = True,\n    # Training options (all tunable parameters)\n    config: str = CONFIGURATION,\n    fold: Union[int, str] = FOLD,\n    planner: str = PLANNER,\n    plans: str = PLANS_NAME,\n    epochs: Optional[Epochs] = EPOCHS,\n    pretrained_weights: Optional[Path] = None,\n    continue_training: bool = False,\n    num_gpus: int = NUM_GPUS,\n    # Inference options\n    save_probabilities: bool = True,\n    # External model (for inference without training)\n    model_path: Optional[Union[str, Path]] = None,\n    # Timeout\n    timeout: Optional[int] = COMMAND_TIMEOUT,\n):\n    \"\"\"\n    Run complete pipeline: setup -> data prep -> preprocess -> train -> predict.\n    \n    Args:\n        max_cases: Limit number of training cases (None = use all)\n        do_preprocess: Run preprocessing step\n        do_train: Run training step\n        do_inference: Run inference step\n        \n        config: nnUNet configuration (3d_fullres, 2d, 3d_lowres, 3d_cascade_fullres)\n        fold: Fold number (0-4) or \"all\" for training on all data\n        planner: Planner class name\n        plans: Plans name matching the planner\n        epochs: Number of training epochs (1, 5, 10, 20, 50, 100, 150, 200, 250, \n                300, 400, 500, 750, 1000, 2000, 4000, 8000). None = 1000\n        pretrained_weights: Path to pretrained weights for fine-tuning\n        continue_training: Continue from last checkpoint\n        num_gpus: Number of GPUs for DDP training\n        \n        save_probabilities: Save probability maps during inference\n        \n        model_path: Path to trained model checkpoint or directory (str or Path).\n                    When provided with do_train=False, parameters (epochs, plans, config, fold)\n                    are auto-extracted from the path if possible.\n                    Example: \"/path/to/nnUNetTrainer_5epochs__nnUNetResEncUNetMPlans__3d_fullres/fold_all/checkpoint_best.pth\"\n        timeout: Command timeout in seconds (None for no timeout)\n    \n    Returns:\n        True if pipeline completed successfully\n    \"\"\"\n    \n    print(\"=\" * 60)\n    print(\"Vesuvius Surface Detection - nnUNet Pipeline\")\n    print(\"=\" * 60)\n    print(f\"Stages: preprocess={do_preprocess}, train={do_train}, inference={do_inference}\")\n    print(f\"Config: {config}, Fold: {fold}, Epochs: {epochs or 1000}, GPUs: {num_gpus}\")\n    \n    # 1. Setup\n    print(\"\\n[1/5] Environment setup...\")\n    setup_environment()\n    \n    # 2. Prepare raw data (always - uses symlinks, fast)\n    print(\"\\n[2/5] Preparing raw dataset (symlinks)...\")\n    raw_dataset_dir = NNUNET_RAW / DATASET_NAME\n    if not raw_dataset_dir.exists():\n        prepare_dataset(INPUT_DIR, max_cases=max_cases, use_symlinks=USE_SYMLINKS)\n    else:\n        print(f\"Raw dataset already exists: {raw_dataset_dir}\")\n    \n    # 3. Preprocessing\n    if do_preprocess:\n        # Check if pre-prepared preprocessed data exists\n        if _link_prepared_preprocessed():\n            print(\"\\n[3/5] Using pre-prepared preprocessed data...\")\n        else:\n            print(\"\\n[3/5] Preprocessing...\")\n            success = run_preprocessing(planner=planner, configurations=[config], timeout=timeout)\n            if not success:\n                print(\"Preprocessing failed!\")\n                return False\n    else:\n        print(\"\\n[3/5] Skipping preprocessing...\")\n        _link_prepared_preprocessed()  # Still link if available\n    \n    # 4. Training\n    if do_train:\n        print(\"\\n[4/5] Training...\")\n        success = run_training(\n            config=config,\n            fold=fold,\n            plans=plans,\n            epochs=epochs,\n            pretrained_weights=pretrained_weights,\n            continue_training=continue_training,\n            num_gpus=num_gpus,\n            timeout=timeout\n        )\n        if not success:\n            print(\"Training failed!\")\n            return False\n    else:\n        print(\"\\n[4/5] Skipping training...\")\n        \n        # Parse model_path to extract configuration if provided\n        if model_path is not None:\n            model_path = Path(model_path) if isinstance(model_path, str) else model_path\n            _, parsed_epochs, parsed_plans, parsed_config, parsed_fold = _parse_model_path(model_path)\n            \n            # Override parameters with parsed values\n            if parsed_epochs is not None:\n                epochs = parsed_epochs\n                print(f\"  Detected epochs={epochs} from model path\")\n            if parsed_plans is not None:\n                plans = parsed_plans\n                print(f\"  Detected plans={plans} from model path\")\n            if parsed_config is not None:\n                config = parsed_config\n                print(f\"  Detected config={config} from model path\")\n            if parsed_fold is not None:\n                fold = parsed_fold\n                print(f\"  Detected fold={fold} from model path\")\n            \n            if model_path.exists():\n                print(f\"Using model: {model_path}\")\n            else:\n                print(f\"WARNING: model_path does not exist: {model_path}\")\n        \n        # Verify model exists\n        expected_model_dir = get_training_output_dir(epochs=epochs, plans=plans, config=config, fold=fold)\n        expected_model_dir.mkdir(exist_ok=True, parents=True)\n        checkpoint_final = expected_model_dir / \"checkpoint_final.pth\"\n        checkpoint_best = expected_model_dir / \"checkpoint_best.pth\"\n\n        if model_path and Path(model_path).exists():\n            # Symlink external -> final so nnUNet finds it\n            checkpoint_final.symlink_to(model_path)\n            print(f\"Map model: {checkpoint_final}\")\n        elif checkpoint_final.exists():\n            print(f\"Found model: {checkpoint_final}\")\n        elif checkpoint_best.exists():\n            # Symlink best -> final so nnUNet finds it\n            checkpoint_final.symlink_to(checkpoint_best)\n            print(f\"Found model: {checkpoint_best} (symlinked to checkpoint_final.pth)\")\n        elif do_inference:\n            print(f\"WARNING: No model found at {expected_model_dir}\")\n            print(\"  Provide model_path to a valid nnUNet checkpoint file\")\n    \n    # 5. Inference\n    if do_inference:\n        print(\"\\n[5/5] Running inference...\")\n        \n        # Prepare test data in temp location\n        test_input_dir = WORKING_DIR / \"test_input\"\n        prepare_test_data(INPUT_DIR, test_input_dir, use_symlinks=USE_SYMLINKS)\n        \n        # Run inference\n        predictions_dir = WORKING_DIR / \"predictions\"\n        success = run_inference(\n            test_input_dir, \n            predictions_dir,\n            config=config,\n            fold=fold,\n            plans=plans,\n            epochs=epochs,\n            save_probabilities=save_probabilities,\n            timeout=timeout\n        )\n        if not success:\n            print(\"Inference failed!\")\n            return False\n        \n        # Convert predictions to TIFF\n        print(\"\\nConverting predictions to TIFF...\")\n        tiff_output_dir = OUTPUT_DIR / \"predictions_tiff\"\n        predictions_to_tiff(predictions_dir, tiff_output_dir, prob_threshold=PROB_THRESHOLD, foreground_label=FOREGROUND_LABEL, min_component_size=MIN_COMPONENT_SIZE, use_hysteresis=USE_HYSTERESIS, hyst_low=HYST_LOW, hyst_high=HYST_HIGH, opening_iters=OPENING_ITERS, border_zero=BORDER_ZERO)\n        \n        print(f\"\\nPredictions saved to: {tiff_output_dir}\")\n    else:\n        print(\"\\n[5/5] Skipping inference...\")\n    \n    print(\"\\n\" + \"=\" * 60)\n    print(\"Pipeline complete!\")\n    print(\"=\" * 60)\n    \n    # Show training progress if training was done\n    if do_train:\n        print(\"\\n[Visualization] Training progress:\")\n        show_progress(epochs=epochs, plans=plans, config=config, fold=fold)\n    \n    # Visualize predictions if inference was done\n    if do_inference:\n        print(\"\\n[Visualization] Sample prediction:\")\n        visualize_predictions(num_samples=1)\n    \n    return True"},{"cell_type":"markdown","id":"ffa449b7","metadata":{"_cell_guid":"19eba4e0-b5fe-4471-a7ac-575a9e869df5","_uuid":"5c82059b-90f5-4cee-8478-c4f74d1ff880","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.004934,"end_time":"2026-01-17T15:56:28.755096","exception":false,"start_time":"2026-01-17T15:56:28.750162","status":"completed"},"tags":[]},"source":"## 11. Create Submission\n\n**Section Summary:**\nCreates a ZIP file for Kaggle submission from prediction TIFF files.\n\n**Expected Format:**\n- ZIP containing .tif prediction files\n- Filenames should match test image names\n- Predictions should be uint8 (0=background, 1=surface)\n\n**Output Location:**\n- `/kaggle/working/submission.zip`"},{"cell_type":"code","execution_count":null,"id":"af084fc9","metadata":{"_cell_guid":"540caf8b-e834-4fc6-9ec9-7ca77ca1ffc0","_uuid":"7defcaab-59f8-4f13-baa4-b1de03be7bd4","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.012052,"end_time":"2026-01-17T15:56:28.772485","exception":false,"start_time":"2026-01-17T15:56:28.760433","status":"completed"},"tags":[]},"outputs":[],"source":"def _tiff_shape_dtype(path: Path) -> Tuple[tuple, np.dtype]:\n    with tifffile.TiffFile(path) as tif:\n        shape = tif.pages[0].shape if len(tif.pages) == 1 else (len(tif.pages), *tif.pages[0].shape)\n        dtype = tif.series[0].dtype\n    return shape, dtype\n\n\ndef validate_submission(\n    predictions_tiff_dir: Path,\n    input_dir: Path = INPUT_DIR,\n    test_csv: Optional[Path] = None,\n    check_shapes: bool = True,\n    check_values: bool = False,\n    max_value_checks: int = 10\n) -> bool:\n    \"\"\"Validate prediction files against test metadata and image shapes.\"\"\"\n    if test_csv is None:\n        test_csv = input_dir / \"test.csv\"\n\n    if test_csv.exists():\n        test_ids = list(pd.read_csv(test_csv)[\"id\"].astype(str))\n    else:\n        test_ids = [p.stem for p in sorted((input_dir / \"test_images\").glob(\"*.tif\"))]\n\n    if not test_ids:\n        print(\"WARNING: No test IDs found for validation\")\n        return False\n\n    missing = []\n    shape_errors = []\n    dtype_errors = []\n    value_errors = []\n\n    for idx, case_id in enumerate(test_ids):\n        pred_path = predictions_tiff_dir / f\"{case_id}.tif\"\n        if not pred_path.exists():\n            missing.append(case_id)\n            continue\n\n        if check_shapes:\n            test_img = input_dir / \"test_images\" / f\"{case_id}.tif\"\n            if test_img.exists():\n                pred_shape, pred_dtype = _tiff_shape_dtype(pred_path)\n                test_shape, test_dtype = _tiff_shape_dtype(test_img)\n                if pred_shape != test_shape:\n                    shape_errors.append(case_id)\n                if pred_dtype != test_dtype:\n                    dtype_errors.append(case_id)\n\n        if check_values and idx < max_value_checks:\n            pred = tifffile.imread(str(pred_path))\n            if not np.isin(pred, [0, 1]).all():\n                value_errors.append(case_id)\n\n    if missing:\n        print(f\"Missing predictions: {len(missing)} (sample: {missing[:5]})\")\n    if shape_errors:\n        print(f\"Shape mismatches: {len(shape_errors)} (sample: {shape_errors[:5]})\")\n    if dtype_errors:\n        print(f\"Dtype mismatches: {len(dtype_errors)} (sample: {dtype_errors[:5]})\")\n    if value_errors:\n        print(f\"Value issues (non-binary): {len(value_errors)} (sample: {value_errors[:5]})\")\n\n    ok = not (missing or shape_errors or dtype_errors or value_errors)\n    if ok:\n        print(\"Validation passed\")\n    return ok\n\n\ndef generate_submission(\n    predictions_tiff_dir: Path = OUTPUT_DIR / \"predictions_tiff\",\n    output_zip: Path = OUTPUT_DIR / \"submission.zip\",\n    delete_after_zip: bool = True,\n    validate: bool = True,\n    strict_validation: bool = False\n) -> Optional[Path]:\n    \"\"\"\n    Create submission ZIP from TIFF predictions.\n    \n    Args:\n        predictions_tiff_dir: Directory containing predicted TIFF files\n        output_zip: Output ZIP file path\n        delete_after_zip: Delete TIFF files after adding to ZIP (saves space)\n        validate: Run basic validation before zipping\n        strict_validation: Abort if validation fails\n    \n    Returns:\n        Path to submission ZIP if successful, None otherwise\n    \"\"\"\n    import zipfile\n    \n    if not predictions_tiff_dir.exists():\n        print(f\"ERROR: Predictions directory not found: {predictions_tiff_dir}\")\n        print(\"Run inference first!\")\n        return None\n\n    if validate:\n        ok = validate_submission(predictions_tiff_dir)\n        if strict_validation and not ok:\n            print(\"Validation failed; aborting submission zip\")\n            return None\n    \n    tiff_files = sorted(predictions_tiff_dir.glob(\"*.tif\"))\n    \n    if not tiff_files:\n        print(f\"No TIFF files found in {predictions_tiff_dir}\")\n        return None\n    \n    print(f\"Creating submission ZIP with {len(tiff_files)} files...\")\n    \n    with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:\n        for tiff_path in tqdm(tiff_files, desc=\"Zipping predictions\"):\n            # Add file with just the filename (no directory structure)\n            zipf.write(tiff_path, tiff_path.name)\n            \n            if delete_after_zip:\n                tiff_path.unlink()\n    \n    zip_size_mb = output_zip.stat().st_size / (1024 * 1024)\n    print(f\"Submission saved: {output_zip} ({zip_size_mb:.1f} MB)\")\n    \n    return output_zip\n\n\n# Example usage:\n# generate_submission()\n# generate_submission(delete_after_zip=True)  # Delete TIFFs after zipping to save space\n"},{"cell_type":"markdown","id":"2ff7e5a6","metadata":{"_cell_guid":"1267b34d-c872-413f-a830-157ec2492bf8","_uuid":"a8d74280-4aad-486e-8f1f-a125033c334d","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.0049,"end_time":"2026-01-17T15:56:28.782322","exception":false,"start_time":"2026-01-17T15:56:28.777422","status":"completed"},"tags":[]},"source":"## 12. Visualization\n\n**Section Summary:**\nFunctions for visualizing training progress and predictions.\n\n**`show_progress()`:**\n- Displays nnUNet's training progress image (loss curves, metrics)\n- Auto-detects correct path based on configuration\n\n**`plot_three_axis_cuts()`:**\n- Shows middle slices of XY, XZ, YZ planes\n- Displays image and mask side-by-side\n\n**`visualize_predictions()`:**\n- Auto-finds predictions and corresponding input images\n- Displays multiple samples if requested"},{"cell_type":"code","execution_count":null,"id":"9e1302da","metadata":{"_cell_guid":"9e7cd5bf-abbf-4c5e-995e-4bd79ee13c0c","_uuid":"16d62e5e-eb9a-4fea-bfa0-60912fb0380a","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.015792,"end_time":"2026-01-17T15:56:28.802949","exception":false,"start_time":"2026-01-17T15:56:28.787157","status":"completed"},"tags":[]},"outputs":[],"source":"def show_progress(\n    epochs: Optional[Epochs] = None,\n    plans: str = PLANS_NAME,\n    config: str = CONFIGURATION,\n    fold: Union[int, str] = FOLD\n):\n    \"\"\"\n    Display training progress image from nnUNet.\n    \n    Args:\n        epochs: Number of epochs used for training (to find correct folder)\n        plans: Plans name\n        config: Configuration name\n        fold: Fold number or \"all\"\n    \"\"\"\n    progress_path = get_progress_image_path(epochs, plans, config, fold)\n    \n    if not progress_path.exists():\n        print(f\"Progress image not found: {progress_path}\")\n        print(\"Training may not have started or completed yet.\")\n        return\n    \n    from IPython.display import Image, display\n    print(f\"Training progress: {progress_path}\")\n    display(Image(filename=str(progress_path)))\n\n\ndef plot_three_axis_cuts(\n    image_vol_path: Path,\n    mask_vol_path: Path,\n    figsize: tuple = (12, 15)\n):\n    \"\"\"\n    Plot middle slices of XY, XZ, and YZ planes for image volume and predicted mask.\n    \n    Args:\n        image_vol_path: Path to input image TIFF\n        mask_vol_path: Path to prediction mask TIFF\n        figsize: Figure size (width, height)\n    \"\"\"\n    print(f\"Visualizing: {image_vol_path.name}\")\n    \n    # Load volumes\n    image_vol = tifffile.imread(str(image_vol_path))\n    mask_vol = tifffile.imread(str(mask_vol_path)).astype(np.uint8)\n    \n    # Get dimensions\n    d, h, w = image_vol.shape\n    z_mid, y_mid, x_mid = d // 2, h // 2, w // 2\n    \n    # Extract slices\n    slices = {\n        'XY Plane (Z-axis)': (image_vol[z_mid, :, :], mask_vol[z_mid, :, :]),\n        'XZ Plane (Y-axis)': (image_vol[:, y_mid, :], mask_vol[:, y_mid, :]),\n        'YZ Plane (X-axis)': (image_vol[:, :, x_mid], mask_vol[:, :, x_mid])\n    }\n    \n    fig, axes = plt.subplots(3, 2, figsize=figsize)\n    for i, (plane_name, (img_slice, mask_slice)) in enumerate(slices.items()):\n        # Image Volume\n        axes[i, 0].imshow(img_slice, cmap='gray')\n        axes[i, 0].set_title(f\"{plane_name} - Image Volume\")\n        axes[i, 0].axis('off')\n        \n        # Mask\n        axes[i, 1].imshow(mask_slice, cmap='gray')\n        axes[i, 1].set_title(f\"{plane_name} - Predicted Mask\")\n        axes[i, 1].axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n\n\ndef visualize_predictions(\n    predictions_dir: Path = OUTPUT_DIR / \"predictions_tiff\",\n    test_images_dir: Path = INPUT_DIR / \"test_images\",\n    num_samples: int = 1\n):\n    \"\"\"\n    Visualize prediction results by showing image/mask pairs.\n    \n    Args:\n        predictions_dir: Directory containing prediction TIFFs\n        test_images_dir: Directory containing input test images\n        num_samples: Number of samples to visualize\n    \"\"\"\n    if not predictions_dir.exists():\n        print(f\"Predictions directory not found: {predictions_dir}\")\n        return\n    \n    predictions = sorted(predictions_dir.glob(\"*.tif\"))\n    if not predictions:\n        print(f\"No TIFF predictions found in {predictions_dir}\")\n        return\n    \n    print(f\"Found {len(predictions)} predictions\")\n    \n    for pred_path in predictions[:num_samples]:\n        image_path = test_images_dir / pred_path.name\n        if image_path.exists():\n            plot_three_axis_cuts(image_path, pred_path)\n        else:\n            print(f\"Warning: Input image not found: {image_path}\")"},{"cell_type":"markdown","id":"e99e8210","metadata":{"_cell_guid":"a87af08c-9922-46c3-9a62-94e2fb7064d8","_uuid":"f58def5b-2797-4470-8310-e82d9eb07f97","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.004947,"end_time":"2026-01-17T15:56:28.812925","exception":false,"start_time":"2026-01-17T15:56:28.807978","status":"completed"},"tags":[]},"source":"## 13. Run Pipeline\n\n**Section Summary:**\nExecute the main pipeline and generate submission.\n\n**Default Configuration:**\n- `fold=\"all\"` - Train on all data (no cross-validation)\n- `config=\"3d_fullres\"` - Best quality single-stage\n- `epochs=1000` - Full training (can reduce to 250-500)\n- `num_gpus=auto` - Uses all available GPUs\n\n**After Running:**\n1. Training progress displayed (progress.png)\n2. Sample prediction visualized\n3. Submission ZIP created at `/kaggle/working/submission.zip`\n\n**提交提示（Kaggle Code Competition）**：默认运行最后一个代码单元即可生成 `submission.zip`。请在右侧 Add data 中添加 `surface-packages-offline`、`vesuvius-surface-nnunet-preprocessed`、`surface-nnunet-checkpoints`。"},{"cell_type":"code","execution_count":null,"id":"d6f7a3aa","metadata":{"_cell_guid":"07aca3f2-b73c-47c9-916c-0683c19971d1","_uuid":"463f2b49-d26e-45c9-9472-d0779a99322e","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":26856.783206,"end_time":"2026-01-17T23:24:05.600969","exception":false,"start_time":"2026-01-17T15:56:28.817763","status":"completed"},"tags":[]},"outputs":[],"source":"# === Submission run cell (inference-only, Kaggle-ready) ===\n# Required Kaggle Datasets (Add in Notebook -> Add data):\n# 1) surface-packages-offline\n# 2) vesuvius-surface-nnunet-preprocessed\n# 3) surface-nnunet-checkpoints\n#\n# This cell will:\n# - link the prepared preprocessed data (fast)\n# - locate a pretrained checkpoint from /kaggle/input/surface-nnunet-checkpoints\n# - run nnUNet inference on the test set\n# - convert predictions to per-volume .tif masks\n# - package as /kaggle/working/submission.zip (required by Kaggle)\n\nsetup_environment()\n\ndef find_checkpoint(root: str = \"/kaggle/input/surface-nnunet-checkpoints\") -> Path:\n    root = Path(root)\n    if not root.exists():\n        raise FileNotFoundError(\n            f\"Missing {root}. Add the Kaggle dataset: surface-nnunet-checkpoints\"\n        )\n    best = sorted(root.rglob(\"checkpoint_best.pth\"))\n    final = sorted(root.rglob(\"checkpoint_final.pth\"))\n    cands = best + final\n    if not cands:\n        raise FileNotFoundError(f\"No checkpoint_*.pth found under {root}\")\n    return cands[0]\n\nckpt = find_checkpoint()\nprint(f\"Using checkpoint: {ckpt}\")\n\n# IMPORTANT: We skip preprocessing & training on Kaggle to fit the 9h limit.\n# Raw dataset prep (symlinks) and prepared preprocessed linking are handled inside full_pipeline.\nok = full_pipeline(\n    do_preprocess=False,\n    do_train=False,\n    do_inference=True,\n    model_path=ckpt,\n)\n\nif not ok:\n    raise RuntimeError(\"Pipeline failed. Scroll up to the first error and fix that step.\")\n\nzip_path = generate_submission(validate=True, strict_validation=False)\nprint(f\"Done. submission.zip is at: {zip_path}\")\n"}],"metadata":{"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"databundleVersionId":15062069,"sourceId":117682,"sourceType":"competition"},{"datasetId":8953198,"sourceId":14066012,"sourceType":"datasetVersion"},{"datasetId":8944423,"sourceId":14330461,"sourceType":"datasetVersion"}],"dockerImageVersionId":31193,"isGpuEnabled":true,"isInternetEnabled":false,"language":"python","sourceType":"notebook"},"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"}},"nbformat":4,"nbformat_minor":5}