{"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":23,"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\nimport csv\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\ndef _detect_blosc2_from_preprocessed(path: Path) -> Optional[str]:\n    if not path.exists():\n        return None\n    if (path / \"dataset.json\").exists():\n        candidates = [path]\n    else:\n        candidates = list(path.glob(\"Dataset*\"))\n    for base in candidates:\n        if list(base.rglob(\"*.b2nd\")):\n            return \"1\"\n        if list(base.rglob(\"*.npz\")):\n            return \"0\"\n    return None\n\n\ndef configure_blosc2() -> str:\n    existing = os.environ.get(\"nnUNet_USE_BLOSC2\")\n    if existing is not None:\n        return existing\n    override = os.environ.get(\"VESUVIUS_BLOSC2\")\n    if override is not None:\n        val = \"1\" if override.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"} else \"0\"\n        os.environ[\"nnUNet_USE_BLOSC2\"] = val\n        return val\n    detected = _detect_blosc2_from_preprocessed(PREPARED_PREPROCESSED_PATH)\n    if detected is None:\n        detected = \"1\"  # default to blosc2 for speed when no prepared data\n    os.environ[\"nnUNet_USE_BLOSC2\"] = detected\n    return detected\n\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# Performance/automation toggles\nAUTO_TUNE_INFERENCE = _env_flag(\"VESUVIUS_AUTO_TUNE_INFERENCE\", True)\nAUTO_SKIP_TRAINING_IF_MODEL_EXISTS = _env_flag(\"VESUVIUS_AUTO_SKIP_TRAIN\", True)\nFAST_TIFF_COPY = _env_flag(\"VESUVIUS_FAST_TIFF_COPY\", True)\nZIP_COMPRESSLEVEL = _env_int(\"VESUVIUS_ZIP_LEVEL\", 1)\nZIP_DELETE_TIFF = _env_flag(\"VESUVIUS_ZIP_DELETE_TIFF\", True)\nDELETE_PROBABILITY_FILES = _env_flag(\"VESUVIUS_DELETE_PROBS\", True)\nTORCH_FAST_MATH = _env_flag(\"VESUVIUS_TORCH_FAST\", True)\n\nMODEL_PATH_ENV = os.environ.get(\"VESUVIUS_MODEL_PATH\", \"\").strip()\nDEFAULT_MODEL_PATH = Path(MODEL_PATH_ENV).expanduser() if MODEL_PATH_ENV else None\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\", \"balanced\").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\", None)\nMIN_COMPONENT_SIZE = _env_int(\"VESUVIUS_MIN_COMPONENT_SIZE\", 0)\n\nAUTO_ENABLE_SAVE_PROBABILITIES = _env_flag(\"VESUVIUS_AUTO_SAVE_PROB\", True)\nif AUTO_ENABLE_SAVE_PROBABILITIES and PROB_THRESHOLD is not None:\n    INFERENCE_SAVE_PROBABILITIES = True\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\n\ndef _count_test_cases(input_dir: Path) -> int:\n    csv_path = input_dir / \"test.csv\"\n    if csv_path.exists():\n        with csv_path.open(newline=\"\") as f:\n            return sum(1 for _ in csv.DictReader(f))\n    img_dir = input_dir / \"test_images\"\n    if img_dir.exists():\n        return len(list(img_dir.glob(\"*.tif\")))\n    return 0\n\n\ndef auto_tune_inference_settings(\n    test_count: int,\n    step_size: Optional[float],\n    disable_tta: bool,\n    save_probabilities: bool,\n    prob_threshold: Optional[float]\n) -> Tuple[Optional[float], bool, bool]:\n    if not AUTO_TUNE_INFERENCE or test_count <= 0:\n        if prob_threshold is not None:\n            save_probabilities = True\n        return step_size, disable_tta, save_probabilities\n\n    tuned_step = step_size\n    tuned_disable_tta = disable_tta\n    tuned_save_probs = save_probabilities\n\n    if test_count >= 80:\n        tuned_step = max(step_size or 0.7, 0.7)\n        tuned_disable_tta = True\n        tuned_save_probs = False\n    elif test_count >= 30:\n        tuned_step = max(step_size or 0.65, 0.65)\n        tuned_disable_tta = True\n    elif test_count >= 5:\n        tuned_step = step_size or 0.6\n    else:\n        tuned_step = step_size or 0.5\n\n    if prob_threshold is not None:\n        tuned_save_probs = True\n\n    return tuned_step, tuned_disable_tta, tuned_save_probs\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":24,"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":[{"name":"stdout","output_type":"stream","text":["INPUT_DIR: /Users/jeremy/Desktop/kaggle/vesuvius-challenge-surface-detection\n","WORKING_DIR: /Users/jeremy/Desktop/kaggle/work\n","OUTPUT_DIR: /Users/jeremy/Desktop/kaggle/outputs\n","nnUNet_raw: /Users/jeremy/Desktop/kaggle/work/nnUNet_data/nnUNet_raw\n","nnUNet_preprocessed: /Users/jeremy/Desktop/kaggle/work/nnUNet_data/nnUNet_preprocessed\n","nnUNet_results: /Users/jeremy/Desktop/kaggle/outputs/nnUNet_results\n","RUN_MODE: balanced\n","TRAIN config: 3d_lowres, fold=all, epochs=500\n","TRAIN_DEVICE: cpu, NUM_GPUS: 1\n","INFER folds: auto\n","INFER step_size: 0.6, disable_tta: False\n","INFER save_probabilities: False\n","INFER use_best_checkpoint: True\n","POST prob_threshold: None, min_component_size: 0\n","USE_SYMLINKS: True\n","nnUNet_USE_BLOSC2: 1 (0=NPZ, 1=blosc2)\n","NUM_WORKERS: 8\n","INFERENCE_NPP: 4\n","INFERENCE_NPS: 4\n"]}],"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    blosc2_setting = configure_blosc2()\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: {blosc2_setting} (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    print(f\"AUTO_TUNE_INFERENCE: {AUTO_TUNE_INFERENCE}\")\n    print(f\"AUTO_SKIP_TRAINING_IF_MODEL_EXISTS: {AUTO_SKIP_TRAINING_IF_MODEL_EXISTS}\")\n    print(f\"FAST_TIFF_COPY: {FAST_TIFF_COPY}\")\n    print(f\"ZIP_COMPRESSLEVEL: {ZIP_COMPRESSLEVEL}\")\n    print(f\"DELETE_PROBABILITY_FILES: {DELETE_PROBABILITY_FILES}\")\n    print(f\"MODEL_PATH: {DEFAULT_MODEL_PATH or 'None'}\")\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":25,"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":[{"name":"stdout","output_type":"stream","text":["Available GPUs: 0\n","Using NUM_GPUS=1\n"]}],"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# Ensure blosc2 setting matches available preprocessed data (if any)\nconfigure_blosc2()\n\nimport nibabel as nib\nimport numpy as np\nimport pandas as pd\nimport tifffile\nimport matplotlib.pyplot as plt\nfrom tqdm.auto import tqdm\n\n# Optional torch performance tuning\nif TORCH_FAST_MATH:\n    try:\n        import torch\n        if torch.cuda.is_available():\n            torch.backends.cuda.matmul.allow_tf32 = True\n            torch.backends.cudnn.allow_tf32 = True\n        torch.backends.cudnn.benchmark = True\n        try:\n            torch.set_float32_matmul_precision(\"medium\")\n        except Exception:\n            pass\n    except Exception as exc:\n        print(f\"Torch perf tuning skipped: {exc}\")\n\n# Show GPU configuration\nprint(f\"Available GPUs: {_get_gpu_count()}\")\nprint(f\"Using NUM_GPUS={NUM_GPUS}\")\n"},{"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":26,"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":27,"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    skip_existing: 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        if json_path is None:\n            json_path = _sidecar_path_for(dest_path)\n\n        if skip_existing and dest_path.exists() and json_path.exists():\n            return True\n\n        if dest_path.is_symlink() and not dest_path.exists():\n            dest_path.unlink()\n\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 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":28,"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":29,"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":30,"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_case(img_path: Path, output_dir: Path, use_symlinks: bool) -> bool:\n    case_id = img_path.stem\n    return 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\ndef prepare_test_data(\n    input_dir: Path,\n    output_dir: Path,\n    use_symlinks: Optional[bool] = None,\n    num_workers: Optional[int] = None\n) -> 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    if num_workers is None:\n        num_workers = NUM_WORKERS\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    print(f\"Processing with {num_workers} workers...\")\n\n    worker = partial(_prepare_test_case, output_dir=output_dir, use_symlinks=use_symlinks)\n\n    if num_workers <= 1:\n        _ = [worker(p) for p in tqdm(test_files, total=len(test_files), desc=\"Preparing test data\")]\n    else:\n        try:\n            with Pool(num_workers) as pool:\n                _ = list(tqdm(\n                    pool.imap(worker, test_files),\n                    total=len(test_files),\n                    desc=\"Preparing test data\"\n                ))\n        except Exception as exc:\n            print(f\"WARNING: multiprocessing failed ({exc}); falling back to single process\")\n            _ = [worker(p) for p in tqdm(test_files, total=len(test_files), desc=\"Preparing test data\")]\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":31,"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\ndef _infer_output_dtype(output_dtype: Optional[Union[str, np.dtype]]) -> np.dtype:\n    if output_dtype is None or (isinstance(output_dtype, str) and output_dtype.strip().lower() in {\"\", \"auto\"}):\n        labels_dir = INPUT_DIR / \"train_labels\"\n        if labels_dir.exists():\n            label_files = sorted(labels_dir.glob(\"*.tif\"))\n            if label_files:\n                with tifffile.TiffFile(label_files[0]) as tif:\n                    return tif.series[0].dtype\n        return np.uint8\n    return np.dtype(output_dtype)\n\n\ndef predictions_to_tiff(\n    pred_dir: Path,\n    output_dir: Path,\n    prob_threshold: Optional[float] = None,\n    foreground_label: int = FOREGROUND_LABEL,\n    min_component_size: int = 0,\n    overwrite: bool = True,\n    output_dtype: Optional[Union[str, np.dtype]] = None,\n    fast_copy: bool = FAST_TIFF_COPY,\n    verify_binary: bool = True,\n    delete_probabilities: bool = DELETE_PROBABILITY_FILES\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    out_dtype = _infer_output_dtype(output_dtype)\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 prob_threshold is not None and probs.ndim == 4 and foreground_label < probs.shape[0]:\n                pred = (probs[foreground_label] >= 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            pred = _postprocess_binary_mask(pred, min_component_size)\n            pred = pred.astype(out_dtype, copy=False)\n            tifffile.imwrite(out_path, pred)\n            if delete_probabilities:\n                try:\n                    npz_path.unlink()\n                except OSError:\n                    pass\n    elif tif_files:\n        if fast_copy and prob_threshold is None and min_component_size <= 0:\n            sample_ok = True\n            try:\n                with tifffile.TiffFile(tif_files[0]) as tif:\n                    if out_dtype is not None and tif.series[0].dtype != out_dtype:\n                        sample_ok = False\n                if verify_binary:\n                    sample = tifffile.imread(str(tif_files[0]))\n                    if not np.isin(sample, [0, 1]).all():\n                        sample_ok = False\n            except Exception:\n                sample_ok = False\n\n            if sample_ok:\n                print(f\"Fast path: copying {len(tif_files)} TIFF prediction files...\")\n                for tif_path in tqdm(tif_files, desc=\"Copying TIFF\"):\n                    out_path = output_dir / f\"{tif_path.stem}.tif\"\n                    if out_path.exists() and not overwrite:\n                        continue\n                    try:\n                        os.link(tif_path, out_path)\n                    except OSError:\n                        shutil.copy2(tif_path, out_path)\n                return\n\n        print(f\"Converting {len(tif_files)} TIFF prediction files...\")\n        for tif_path in tqdm(tif_files, desc=\"Converting 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))\n            pred = _to_binary_mask(pred, foreground_label)\n            pred = _postprocess_binary_mask(pred, min_component_size)\n            pred = pred.astype(out_dtype, copy=False)\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 = _postprocess_binary_mask(pred, min_component_size)\n            pred = pred.astype(out_dtype, copy=False)\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":32,"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":33,"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 _ensure_checkpoint_link(src: Path, dst: Path) -> None:\n    \"\"\"Symlink src -> dst if dst missing or stale symlink.\"\"\"\n    if dst.exists():\n        if dst.is_symlink() and dst.resolve() == src.resolve():\n            return\n        if dst.is_symlink():\n            dst.unlink()\n        else:\n            print(f\"WARNING: {dst} exists and is not a symlink; not overwriting\")\n            return\n    dst.symlink_to(src)\n\n\ndef _ensure_model_metadata(external_base: Path, local_base: Path) -> None:\n    \"\"\"Copy dataset/plans metadata into local model base if missing.\"\"\"\n    local_base.mkdir(parents=True, exist_ok=True)\n    for name in (\"dataset.json\", \"plans.json\"):\n        src = external_base / name\n        dst = local_base / name\n        if src.exists() and not dst.exists():\n            shutil.copy2(src, dst)\n\n\ndef 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    train_device: str = TRAIN_DEVICE,\n    # Inference options\n    save_probabilities: bool = INFERENCE_SAVE_PROBABILITIES,\n    inference_folds: Optional[Union[int, str, Iterable[int]]] = INFERENCE_FOLDS,\n    inference_step_size: Optional[float] = INFERENCE_STEP_SIZE,\n    disable_tta: bool = INFERENCE_DISABLE_TTA,\n    use_best_checkpoint: bool = INFERENCE_USE_BEST_CHECKPOINT,\n    continue_prediction: bool = INFERENCE_CONTINUE_PREDICTION,\n    inference_device: str = INFERENCE_DEVICE,\n    prob_threshold: Optional[float] = PROB_THRESHOLD,\n    min_component_size: int = MIN_COMPONENT_SIZE,\n    # External model (for inference without training)\n    model_path: Optional[Union[str, Path]] = DEFAULT_MODEL_PATH,\n    # Timeout\n    timeout: Optional[int] = COMMAND_TIMEOUT,\n):\n    \"\"\"\n    Run complete pipeline: setup -> data prep -> preprocess -> train -> predict.\n    \"\"\"\n\n    print(\"=\" * 60)\n    print(\"Vesuvius Surface Detection - nnUNet Pipeline\")\n    print(\"=\" * 60)\n    print(f\"RUN_MODE: {RUN_MODE}\")\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    print(f\"Train device: {train_device}, Inference device: {inference_device}\")\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    # Auto-skip training if checkpoint already exists\n    if do_train and AUTO_SKIP_TRAINING_IF_MODEL_EXISTS:\n        expected_model_dir = get_training_output_dir(epochs=epochs, plans=plans, config=config, fold=fold)\n        if (expected_model_dir / \"checkpoint_final.pth\").exists() or (expected_model_dir / \"checkpoint_best.pth\").exists():\n            print(\"\n[4/5] Auto-skip training: checkpoint already exists\")\n            do_train = False\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            device=train_device,\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        external_base = None\n        if model_path is not None:\n            model_path = Path(model_path) if isinstance(model_path, str) else model_path\n            model_dir_from_path, 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            if model_dir_from_path is not None:\n                if model_dir_from_path.name.startswith(\"fold_\"):\n                    external_base = model_dir_from_path.parent\n                else:\n                    external_base = model_dir_from_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            model_path = Path(model_path)\n            if model_path.is_file():\n                _ensure_checkpoint_link(model_path, checkpoint_final)\n                print(f\"Map model: {checkpoint_final}\")\n            else:\n                candidate_final = model_path / \"checkpoint_final.pth\"\n                candidate_best = model_path / \"checkpoint_best.pth\"\n                if candidate_final.exists():\n                    _ensure_checkpoint_link(candidate_final, checkpoint_final)\n                    print(f\"Map model: {checkpoint_final}\")\n                elif candidate_best.exists():\n                    _ensure_checkpoint_link(candidate_best, checkpoint_final)\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            _ensure_checkpoint_link(checkpoint_best, checkpoint_final)\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        if external_base is not None:\n            _ensure_model_metadata(external_base, get_model_base_dir(epochs=epochs, plans=plans, config=config))\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, num_workers=NUM_WORKERS)\n\n        test_count = _count_test_cases(INPUT_DIR)\n        tuned_step, tuned_disable_tta, tuned_save_probs = auto_tune_inference_settings(\n            test_count, inference_step_size, disable_tta, save_probabilities, prob_threshold\n        )\n        if AUTO_TUNE_INFERENCE:\n            print(f\"AUTO_TUNE_INFERENCE: test_count={test_count} => step_size={tuned_step}, disable_tta={tuned_disable_tta}, save_probabilities={tuned_save_probs}\")\n        inference_step_size = tuned_step\n        disable_tta = tuned_disable_tta\n        save_probabilities = tuned_save_probs\n\n        if prob_threshold is not None and not save_probabilities:\n            print(\"NOTE: prob_threshold set but save_probabilities=False; using argmax outputs.\")\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=inference_folds,\n            plans=plans,\n            epochs=epochs,\n            save_probabilities=save_probabilities,\n            step_size=inference_step_size,\n            disable_tta=disable_tta,\n            use_best_checkpoint=use_best_checkpoint,\n            continue_prediction=continue_prediction,\n            device=inference_device,\n            num_processes_preprocessing=INFERENCE_NUM_PROCESSES_PREPROCESSING,\n            num_processes_segmentation=INFERENCE_NUM_PROCESSES_SEGMENTATION,\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(\n            predictions_dir,\n            tiff_output_dir,\n            prob_threshold=prob_threshold,\n            foreground_label=FOREGROUND_LABEL,\n            min_component_size=min_component_size,\n            fast_copy=FAST_TIFF_COPY,\n            delete_probabilities=DELETE_PROBABILITY_FILES\n        )\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\n"},{"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":34,"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 _get_train_label_dtype(input_dir: Path) -> Optional[np.dtype]:\n    labels_dir = input_dir / \"train_labels\"\n    if not labels_dir.exists():\n        return None\n    label_files = sorted(labels_dir.glob(\"*.tif\"))\n    if not label_files:\n        return None\n    _, dtype = _tiff_shape_dtype(label_files[0])\n    return 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    label_dtype = _get_train_label_dtype(input_dir)\n    if label_dtype is not None:\n        print(f\"Expected mask dtype (from train_labels): {label_dtype}\")\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                expected_dtype = label_dtype or test_dtype\n                if expected_dtype is not None and pred_dtype != expected_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 = ZIP_DELETE_TIFF,\n    validate: bool = True,\n    strict_validation: bool = False,\n    compresslevel: Optional[int] = ZIP_COMPRESSLEVEL\n) -> Optional[Path]:\n    \"\"\"\n    Create submission ZIP from TIFF predictions.\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    compression = zipfile.ZIP_DEFLATED\n    zip_kwargs = {}\n    if compresslevel is not None:\n        try:\n            level = int(compresslevel)\n            if level <= 0:\n                compression = zipfile.ZIP_STORED\n            else:\n                zip_kwargs[\"compresslevel\"] = level\n        except (TypeError, ValueError):\n            pass\n\n    with zipfile.ZipFile(output_zip, 'w', compression=compression, **zip_kwargs) 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":35,"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`"},{"cell_type":"code","execution_count":36,"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":[{"name":"stdout","output_type":"stream","text":["============================================================\n","Vesuvius Surface Detection - nnUNet Pipeline\n","============================================================\n","RUN_MODE: balanced\n","Stages: preprocess=True, train=True, inference=True\n","Config: 3d_lowres, Fold: all, Epochs: 500, GPUs: 1\n","Train device: cpu, Inference device: cpu\n","\n","[1/5] Environment setup...\n","INPUT_DIR: /Users/jeremy/Desktop/kaggle/vesuvius-challenge-surface-detection\n","WORKING_DIR: /Users/jeremy/Desktop/kaggle/work\n","OUTPUT_DIR: /Users/jeremy/Desktop/kaggle/outputs\n","nnUNet_raw: /Users/jeremy/Desktop/kaggle/work/nnUNet_data/nnUNet_raw\n","nnUNet_preprocessed: /Users/jeremy/Desktop/kaggle/work/nnUNet_data/nnUNet_preprocessed\n","nnUNet_results: /Users/jeremy/Desktop/kaggle/outputs/nnUNet_results\n","RUN_MODE: balanced\n","TRAIN config: 3d_lowres, fold=all, epochs=500\n","TRAIN_DEVICE: cpu, NUM_GPUS: 1\n","INFER folds: auto\n","INFER step_size: 0.6, disable_tta: False\n","INFER save_probabilities: False\n","INFER use_best_checkpoint: True\n","POST prob_threshold: None, min_component_size: 0\n","USE_SYMLINKS: True\n","nnUNet_USE_BLOSC2: 1 (0=NPZ, 1=blosc2)\n","NUM_WORKERS: 8\n","INFERENCE_NPP: 4\n","INFERENCE_NPS: 4\n","\n","[2/5] Preparing raw dataset (symlinks)...\n","Raw dataset already exists: /Users/jeremy/Desktop/kaggle/work/nnUNet_data/nnUNet_raw/Dataset100_VesuviusSurface\n","\n","[3/5] Preprocessing...\n","Running: nnUNetv2_plan_and_preprocess -d 100 -np 8 -pl nnUNetPlannerResEncM -c 3d_lowres\n","Preprocessing FAILED!\n","STDERR:\n","Traceback (most recent call last):\n","  File \"/opt/anaconda3/bin/nnUNetv2_plan_and_preprocess\", line 3, in <module>\n","    from nnunetv2.experiment_planning.plan_and_preprocess_entrypoints import plan_and_preprocess_entry\n","  File \"/opt/anaconda3/lib/python3.12/site-packages/nnunetv2/experiment_planning/plan_and_preprocess_entrypoints.py\", line 2, in <module>\n","    from nnunetv2.experiment_planning.plan_and_preprocess_api import extract_fingerprints, plan_experiments, preprocess\n","  File \"/opt/anaconda3/lib/python3.12/site-packages/nnunetv2/experiment_planning/plan_and_preprocess_api.py\", line 8, in <module>\n","    from nnunetv2.experiment_planning.dataset_fingerprint.fingerprint_extractor import DatasetFingerprintExtractor\n","  File \"/opt/anaconda3/lib/python3.12/site-packages/nnunetv2/experiment_planning/dataset_fingerprint/fingerprint_extractor.py\", line 11, in <module>\n","    from nnunetv2.imageio.reader_writer_registry import determine_reader_writer_from_dataset_json\n","  File \"/opt/anaconda3/lib/python3.12/site-packages/nnunetv2/imageio/reader_writer_registry.py\", line 7, in <module>\n","    from nnunetv2.imageio.natural_image_reader_writer import NaturalImage2DIO\n","  File \"/opt/anaconda3/lib/python3.12/site-packages/nnunetv2/imageio/natural_image_reader_writer.py\", line 19, in <module>\n","    from skimage import io\n","  File \"/opt/anaconda3/lib/python3.12/site-packages/skimage/__init__.py\", line 124, in <module>\n","    from ._shared import geometry\n","  File \"geometry.pyx\", line 1, in init skimage._shared.geometry\n","ValueError: numpy.dtype size changed, may indicate binary incompatibility. Expected 96 from C header, got 88 from PyObject\n","\n","Preprocessing failed!\n","ERROR: Predictions directory not found: /Users/jeremy/Desktop/kaggle/outputs/predictions_tiff\n","Run inference first!\n"]}],"source":"# Sample pipeline using RUN_MODE defaults\nfull_pipeline()\n\n# Then resume when training finishes and run inference with the best model\n# full_pipeline(\n#     do_preprocess=False, do_train=False,\n#     model_path=\"/kaggle/input/surface-nnunet-checkpoints/pytorch/default/.../checkpoint_best.pth\",\n# )\n\n# Generate submission ZIP\ngenerate_submission()\n"},{"cell_type":"markdown","id":"9e5f6a63","metadata":{},"source":"## 14. 效率测试\n\n**部分总结:**\n测试pipeline各个阶段的执行效率。\n\n**测试配置:**\n- 小规模数据集（10个案例）\n- 快速配置（2d或3d_lowres）\n- 少量epochs（5-10）\n- 记录每个阶段的执行时间\n\n**输出指标:**\n- 数据准备时间\n- 预处理时间\n- 单个epoch训练时间\n- 推理时间\n- 总体吞吐量\n"},{"cell_type":"code","execution_count":22,"id":"72b77a64","metadata":{},"outputs":[],"source":"import time\nimport subprocess\nfrom datetime import datetime, timedelta\n\nclass EfficiencyTester:\n    \"\"\"效率测试工具 - 测量pipeline各阶段的执行时间\"\"\"\n    \n    def __init__(self):\n        self.results = {}\n        self.start_time = None\n        \n    def start(self):\n        self.start_time = time.time()\n        \n    def mark(self, stage_name: str):\n        \"\"\"标记阶段完成时间\"\"\"\n        elapsed = time.time() - self.start_time\n        self.results[stage_name] = elapsed\n        return elapsed\n    \n    def get_summary(self):\n        \"\"\"生成效率报告\"\"\"\n        print(\"\\n\" + \"=\" * 70)\n        print(\"⏱️  效率测试报告\")\n        print(\"=\" * 70)\n        \n        total_time = sum(self.results.values())\n        \n        for stage, duration in self.results.items():\n            pct = (duration / total_time * 100) if total_time > 0 else 0\n            bar = \"█\" * int(pct / 2.5)\n            print(f\"{stage:25} | {duration:7.1f}s | {bar:20} {pct:5.1f}%\")\n        \n        print(\"-\" * 70)\n        print(f\"{'总耗时':25} | {total_time:7.1f}s |\")\n        print(\"=\" * 70)\n        \n        return self.results, total_time\n\n\ndef run_efficiency_test(\n    max_train_cases: int = 10,  # 只用10个案例测试\n    config: str = \"2d\",  # 用2d配置 - 最快\n    epochs: int = 5,  # 只训练5个epoch\n):\n    \"\"\"\n    运行快速效率测试。\n    \n    这会在小数据集上运行完整pipeline，记录每个阶段的执行时间。\n    \"\"\"\n    \n    tester = EfficiencyTester()\n    \n    print(\"\\n\" + \"=\" * 70)\n    print(\"🚀 启动效率测试\")\n    print(\"=\" * 70)\n    print(f\"配置:\")\n    print(f\"  - 训练案例数: {max_train_cases}\")\n    print(f\"  - 配置: {config}\")\n    print(f\"  - Epochs: {epochs}\")\n    print(f\"  - GPU数量: {NUM_GPUS}\")\n    print(\"=\" * 70)\n    \n    tester.start()\n    \n    # 1. 数据准备\n    print(\"\\n[1/4] 准备原始数据...\")\n    raw_dataset_dir = NNUNET_RAW / DATASET_NAME\n    if not raw_dataset_dir.exists():\n        prepare_dataset(INPUT_DIR, max_cases=max_train_cases, use_symlinks=USE_SYMLINKS)\n    else:\n        print(\"  ✓ 原始数据已存在\")\n    elapsed = tester.mark(\"1. 数据准备\")\n    print(f\"  ✓ 完成 ({elapsed:.1f}s)\")\n    \n    # 2. 预处理\n    print(\"\\n[2/4] 运行预处理...\")\n    if _link_prepared_preprocessed():\n        print(\"  ✓ 使用预准备的预处理数据\")\n        elapsed = tester.mark(\"2. 预处理\")\n    else:\n        success = run_preprocessing(\n            planner=PLANNER,\n            configurations=[config],\n            timeout=None\n        )\n        elapsed = tester.mark(\"2. 预处理\")\n        if not success:\n            print(\"  ✗ 预处理失败!\")\n            return tester\n        print(f\"  ✓ 完成 ({elapsed:.1f}s)\")\n    \n    # 3. 训练\n    print(f\"\\n[3/4] 训练 ({epochs} epochs)...\")\n    success = run_training(\n        config=config,\n        fold=\"all\",  # 用所有数据训练\n        epochs=epochs,\n        num_gpus=NUM_GPUS,\n        timeout=None\n    )\n    elapsed = tester.mark(\"3. 训练\")\n    if not success:\n        print(\"  ✗ 训练失败!\")\n        return tester\n    \n    avg_epoch_time = elapsed / epochs\n    print(f\"  ✓ 完成 ({elapsed:.1f}s, 平均 {avg_epoch_time:.1f}s/epoch)\")\n    \n    # 4. 推理\n    print(\"\\n[4/4] 运行推理...\")\n    test_input_dir = WORKING_DIR / \"test_input_efficiency\"\n    prepare_test_data(INPUT_DIR, test_input_dir, use_symlinks=USE_SYMLINKS)\n    \n    predictions_dir = WORKING_DIR / \"predictions_efficiency\"\n    success = run_inference(\n        test_input_dir,\n        predictions_dir,\n        config=config,\n        fold=\"all\",\n        epochs=epochs,\n        save_probabilities=False,\n        timeout=None\n    )\n    elapsed = tester.mark(\"4. 推理\")\n    if not success:\n        print(\"  ✗ 推理失败!\")\n        return tester\n    \n    test_count = len(list(test_input_dir.glob(\"*_0000.tif\")))\n    avg_case_time = elapsed / max(test_count, 1)\n    print(f\"  ✓ 完成 ({elapsed:.1f}s, 平均 {avg_case_time:.1f}s/个)\")\n    \n    # 显示完整报告\n    results, total = tester.get_summary()\n    \n    # 额外的性能指标\n    print(\"\\n\" + \"=\" * 70)\n    print(\"📊 性能指标\")\n    print(\"=\" * 70)\n    print(f\"训练吞吐量: {max_train_cases / (results['3. 训练'] / epochs):.2f} cases/epoch/s\")\n    print(f\"推理吞吐量: {test_count / elapsed:.2f} cases/s\")\n    print(f\"每个epoch所需时间: {avg_epoch_time:.1f}s\")\n    print(f\"预处理占比: {results['2. 预处理'] / total * 100:.1f}%\")\n    print(\"=\" * 70 + \"\\n\")\n    \n    return tester\n\n\nRUN_EFFICIENCY_TEST = False  # Set True to run the efficiency test\n\nif RUN_EFFICIENCY_TEST:\n    # 运行效率测试 (预计5-15分钟取决于GPU)\n    test_results = run_efficiency_test(\n        max_train_cases=10,  # 用10个案例测试\n        config=\"2d\",         # 最快的配置\n        epochs=5             # 快速测试\n    )\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 [conda env:base] *","language":"python","name":"conda-base-py"},"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"},"papermill":{"default_parameters":{},"duration":26951.917964,"end_time":"2026-01-17T23:24:08.384668","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-17T15:54:56.466704","version":"2.6.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"088ba08e11534ac0b5b6db704d66a996":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_353c849eebe54e26a683cdd51fdc148c","placeholder":"​","style":"IPY_MODEL_f84a28619b2f442c85181203bfa0880f","tabbable":null,"tooltip":null,"value":"Zipping predictions: 100%"}},"129dcca0d0ce4c86a77274997e28f57d":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_97400aa146544135b913144fa9b26917","placeholder":"​","style":"IPY_MODEL_84f3976d82e64a91810dfadd10e4a5a7","tabbable":null,"tooltip":null,"value":" 1/1 [00:00&lt;00:00,  2.25it/s]"}},"18b6d17f402847a3878879b934a1f1ea":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_9aab94c49c6a420fb6be165e8a4fe32d","placeholder":"​","style":"IPY_MODEL_f21c03626de4483db6fb76ed3477b1b8","tabbable":null,"tooltip":null,"value":"Converting to TIFF: 100%"}},"1c0528fd14cc4036b3df751e1df673f0":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_3c9a7ef06dde41d6baaf2738898f1484","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_d7481e3410de4e4f91933292f0b5341d","tabbable":null,"tooltip":null,"value":1}},"1c52d83ae0e04cc7b4b6ea9437921afd":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"21461aa541af4358b4f53d72a9feac40":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_e88be2b5db5042e99a2d24e0cbb47c2e","placeholder":"​","style":"IPY_MODEL_465ccbc2e1c24be8a38d63fe12ba276e","tabbable":null,"tooltip":null,"value":" 786/786 [01:12&lt;00:00, 10.80it/s]"}},"26ecebefdec84a818fa7d0f73ab3e535":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"353c849eebe54e26a683cdd51fdc148c":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"3c9a7ef06dde41d6baaf2738898f1484":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"40db6be0a95d46a2b4eb20a83997e4f7":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_f7b9b26cde7f4396b63003b3245fc65e","IPY_MODEL_ac58ee88032a42289cad22be24d683d1","IPY_MODEL_21461aa541af4358b4f53d72a9feac40"],"layout":"IPY_MODEL_6d1cb651802749669a29629c04fbd307","tabbable":null,"tooltip":null}},"465ccbc2e1c24be8a38d63fe12ba276e":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"563fe257b629400c82c2698f0744fe7e":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_bc1377b8ce7346ca886f24dabaf7a57b","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_9bfee97ccc4e42079d9e5d7de4550164","tabbable":null,"tooltip":null,"value":1}},"5dc26c2e04c84fc8b27190c9f5be20f2":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"6561b82f3f2241048edb8ef75ce5dad1":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"66a01fddefb94c21bbdc8d8843f77f5d":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"6d1cb651802749669a29629c04fbd307":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"6f0baae2f7d846498db84c1d637f303e":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"73956007cfe340ffbe56212df9057b75":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"84f3976d82e64a91810dfadd10e4a5a7":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"8804d9a1c99845afbc30c444e6513ca8":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_99c2aa7dde6d42e988c99b93d00188f5","placeholder":"​","style":"IPY_MODEL_1c52d83ae0e04cc7b4b6ea9437921afd","tabbable":null,"tooltip":null,"value":" 1/1 [00:00&lt;00:00,  2.75it/s]"}},"8aba5114a92c405ebd7d4d46664b22a2":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_bb35557436e44b75ba75fd3d6d3db3d7","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_d706fca370b6426c81f8ffa7d353c350","tabbable":null,"tooltip":null,"value":1}},"937eea88b09040af95b269194acd461e":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"93da3e062db64f4a8adba7a3ddeffe5d":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"9643849bfb5b4fa09bc81cfa9406e151":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_e16ee39b5c544555927d2bd299394979","IPY_MODEL_1c0528fd14cc4036b3df751e1df673f0","IPY_MODEL_129dcca0d0ce4c86a77274997e28f57d"],"layout":"IPY_MODEL_26ecebefdec84a818fa7d0f73ab3e535","tabbable":null,"tooltip":null}},"96a431b031404e32b773878d86e41d98":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_088ba08e11534ac0b5b6db704d66a996","IPY_MODEL_8aba5114a92c405ebd7d4d46664b22a2","IPY_MODEL_8804d9a1c99845afbc30c444e6513ca8"],"layout":"IPY_MODEL_73956007cfe340ffbe56212df9057b75","tabbable":null,"tooltip":null}},"97400aa146544135b913144fa9b26917":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"99c2aa7dde6d42e988c99b93d00188f5":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"9aab94c49c6a420fb6be165e8a4fe32d":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"9bfee97ccc4e42079d9e5d7de4550164":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"ac58ee88032a42289cad22be24d683d1":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_937eea88b09040af95b269194acd461e","max":786,"min":0,"orientation":"horizontal","style":"IPY_MODEL_c2bb0c1419c647408b22bb793f747457","tabbable":null,"tooltip":null,"value":786}},"bb35557436e44b75ba75fd3d6d3db3d7":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"bc1377b8ce7346ca886f24dabaf7a57b":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"c2bb0c1419c647408b22bb793f747457":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"cc7774c9dfe84d1bb1ce9f8ab72817bc":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"d706fca370b6426c81f8ffa7d353c350":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"d7481e3410de4e4f91933292f0b5341d":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"d9fa1ab11e1b4f4689f856fec49ffa85":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"e16ee39b5c544555927d2bd299394979":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_6f0baae2f7d846498db84c1d637f303e","placeholder":"​","style":"IPY_MODEL_5dc26c2e04c84fc8b27190c9f5be20f2","tabbable":null,"tooltip":null,"value":"Preparing test data: 100%"}},"e88be2b5db5042e99a2d24e0cbb47c2e":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"f21c03626de4483db6fb76ed3477b1b8":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"f7b9b26cde7f4396b63003b3245fc65e":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_93da3e062db64f4a8adba7a3ddeffe5d","placeholder":"​","style":"IPY_MODEL_d9fa1ab11e1b4f4689f856fec49ffa85","tabbable":null,"tooltip":null,"value":"Preparing dataset: 100%"}},"f84a28619b2f442c85181203bfa0880f":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"fd915b0eb0b1421dbf7bca388de9d13a":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_18b6d17f402847a3878879b934a1f1ea","IPY_MODEL_563fe257b629400c82c2698f0744fe7e","IPY_MODEL_ff955e9ebe7e40f2b8ca3a6209342276"],"layout":"IPY_MODEL_6561b82f3f2241048edb8ef75ce5dad1","tabbable":null,"tooltip":null}},"ff955e9ebe7e40f2b8ca3a6209342276":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_66a01fddefb94c21bbdc8d8843f77f5d","placeholder":"​","style":"IPY_MODEL_cc7774c9dfe84d1bb1ce9f8ab72817bc","tabbable":null,"tooltip":null,"value":" 1/1 [00:02&lt;00:00,  2.23s/it]"}}},"version_major":2,"version_minor":0}}},"nbformat":4,"nbformat_minor":5}