{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":117682,"databundleVersionId":14443416,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":123.948917,"end_time":"2025-11-17T17:09:42.495317","environment_variables":{},"exception":true,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-11-17T17:07:38.5464","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Vesuvius Challenge - 3D Deep Learning Solution\n## Winning Strategy by Hemanth Reganti\n\n**Approach:**\n- 3D U-Net with topology-aware loss\n- Mixed precision training\n- Test-time augmentation\n- Ensemble predictions\n\n**Target Score:** 0.45+ (Beat current leader 0.404)","metadata":{"papermill":{"duration":0.003355,"end_time":"2025-11-17T17:07:42.142284","exception":false,"start_time":"2025-11-17T17:07:42.138929","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Install required packages - MONAI first to avoid conflicts\n!pip install -q monai\n!pip install -q segmentation-models-pytorch albumentations timm\n# Force NumPy 1.x AFTER all packages to prevent it from being overridden\n!pip install -q --force-reinstall 'numpy<2'","metadata":{"execution":{"iopub.execute_input":"2025-11-17T17:07:42.148701Z","iopub.status.busy":"2025-11-17T17:07:42.14844Z","iopub.status.idle":"2025-11-17T17:09:02.160279Z","shell.execute_reply":"2025-11-17T17:09:02.159298Z"},"papermill":{"duration":80.017473,"end_time":"2025-11-17T17:09:02.162444","exception":false,"start_time":"2025-11-17T17:07:42.144971","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom pathlib import Path\nfrom PIL import Image, ImageSequence\nimport zipfile\nfrom io import BytesIO\nfrom tqdm import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom sklearn.model_selection import train_test_split\n\n# MONAI for 3D medical imaging\nfrom monai.networks.nets import UNet\nfrom monai.losses import DiceLoss, DiceCELoss\nfrom monai.transforms import (\n    Compose, RandFlipd, RandRotate90d, RandGaussianNoised,\n    RandAdjustContrastd, RandScaleIntensityd\n)\n\nprint(f'PyTorch version: {torch.__version__}')\nprint(f'CUDA available: {torch.cuda.is_available()}')\nif torch.cuda.is_available():\n    print(f'GPU: {torch.cuda.get_device_name(0)}')","metadata":{"execution":{"iopub.execute_input":"2025-11-17T17:09:02.237282Z","iopub.status.busy":"2025-11-17T17:09:02.236995Z","iopub.status.idle":"2025-11-17T17:09:37.858091Z","shell.execute_reply":"2025-11-17T17:09:37.856925Z"},"papermill":{"duration":35.666901,"end_time":"2025-11-17T17:09:37.860012","exception":false,"start_time":"2025-11-17T17:09:02.193111","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Configuration","metadata":{"papermill":{"duration":0.022374,"end_time":"2025-11-17T17:09:37.904448","exception":false,"start_time":"2025-11-17T17:09:37.882074","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class Config:\n    # Paths\n    TRAIN_DIR = Path('/kaggle/input/vesuvius-challenge-surface-detection/train_images')\n    TRAIN_LABELS = Path('/kaggle/input/vesuvius-challenge-surface-detection/train_labels')\n    TEST_DIR = Path('/kaggle/input/vesuvius-challenge-surface-detection/test_images')\n    TRAIN_CSV = Path('/kaggle/input/vesuvius-challenge-surface-detection/train.csv')\n    TEST_CSV = Path('/kaggle/input/vesuvius-challenge-surface-detection/test.csv')\n    OUTPUT_DIR = Path('/kaggle/working')\n    \n    # Training\n    EPOCHS = 20\n    BATCH_SIZE = 1  # Set to 1 to handle variable-sized volumes\n    LR = 1e-4\n    WEIGHT_DECAY = 1e-5\n    DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n    MIXED_PRECISION = True\n    \n    # Model\n    IN_CHANNELS = 1\n    OUT_CHANNELS = 1\n    SPATIAL_DIMS = 3\n    CHANNELS = (16, 32, 64, 128, 256)\n    STRIDES = (2, 2, 2, 2)\n    \n    # Data\n    PATCH_SIZE = (64, 64, 64)  # 3D patches\n    NUM_WORKERS = 0  # Set to 0 to avoid MONAI MetaTensor collation issues\n    \n    # Augmentation\n    AUG_PROB = 0.5\n    \ncfg = Config()\nprint(f'Device: {cfg.DEVICE}')","metadata":{"execution":{"iopub.execute_input":"2025-11-17T17:09:37.950552Z","iopub.status.busy":"2025-11-17T17:09:37.949494Z","iopub.status.idle":"2025-11-17T17:09:37.955883Z","shell.execute_reply":"2025-11-17T17:09:37.955229Z"},"papermill":{"duration":0.030268,"end_time":"2025-11-17T17:09:37.956959","exception":false,"start_time":"2025-11-17T17:09:37.926691","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dataset","metadata":{"papermill":{"duration":0.022049,"end_time":"2025-11-17T17:09:38.001295","exception":false,"start_time":"2025-11-17T17:09:37.979246","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def load_tif_volume(path):\n    \"\"\"Load 3D TIF volume.\"\"\"\n    with Image.open(path) as tif:\n        frames = [np.array(frame) for frame in ImageSequence.Iterator(tif)]\n    return np.stack(frames, axis=0)\n\nclass VesuviusDataset(Dataset):\n    def __init__(self, df, img_dir, label_dir=None, transforms=None, is_train=True):\n        self.df = df\n        self.img_dir = Path(img_dir)\n        self.label_dir = Path(label_dir) if label_dir else None\n        self.transforms = transforms\n        self.is_train = is_train\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        vol_id = row['id']\n        \n        # Load image\n        img = load_tif_volume(self.img_dir / f'{vol_id}.tif')\n        img = img.astype(np.float32) / 255.0\n        img = np.expand_dims(img, 0)  # Add channel dim\n        \n        if self.is_train and self.label_dir:\n            # Load label\n            label = load_tif_volume(self.label_dir / f'{vol_id}.tif')\n            label = (label == 1).astype(np.float32)  # Binary: 1=foreground\n            label = np.expand_dims(label, 0)\n            \n            if self.transforms:\n                data = {'image': img, 'label': label}\n                data = self.transforms(data)\n                img, label = data['image'], data['label']\n            \n            # FIX: Handle both tensor and numpy array types\n            if isinstance(img, torch.Tensor):\n                img_tensor = img\n            else:\n                img_tensor = torch.from_numpy(img)\n            \n            if isinstance(label, torch.Tensor):\n                label_tensor = label\n            else:\n                label_tensor = torch.from_numpy(label)\n            \n            return img_tensor, label_tensor\n        \n        return torch.from_numpy(img), vol_id","metadata":{"execution":{"iopub.execute_input":"2025-11-17T17:09:38.047155Z","iopub.status.busy":"2025-11-17T17:09:38.046472Z","iopub.status.idle":"2025-11-17T17:09:38.053708Z","shell.execute_reply":"2025-11-17T17:09:38.053076Z"},"papermill":{"duration":0.031222,"end_time":"2025-11-17T17:09:38.054796","exception":false,"start_time":"2025-11-17T17:09:38.023574","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Augmentation","metadata":{"papermill":{"duration":0.021886,"end_time":"2025-11-17T17:09:38.098478","exception":false,"start_time":"2025-11-17T17:09:38.076592","status":"completed"},"tags":[]}},{"cell_type":"code","source":"train_transforms = Compose([\n    RandFlipd(keys=['image', 'label'], spatial_axis=0, prob=cfg.AUG_PROB),\n    RandFlipd(keys=['image', 'label'], spatial_axis=1, prob=cfg.AUG_PROB),\n    RandFlipd(keys=['image', 'label'], spatial_axis=2, prob=cfg.AUG_PROB),\n    RandRotate90d(keys=['image', 'label'], prob=cfg.AUG_PROB, spatial_axes=(1, 2)),\n    RandGaussianNoised(keys=['image'], prob=0.3, std=0.01),\n    RandAdjustContrastd(keys=['image'], prob=0.3, gamma=(0.8, 1.2)),\n    RandScaleIntensityd(keys=['image'], factors=0.1, prob=0.3),\n])","metadata":{"execution":{"iopub.execute_input":"2025-11-17T17:09:38.14278Z","iopub.status.busy":"2025-11-17T17:09:38.142578Z","iopub.status.idle":"2025-11-17T17:09:38.150461Z","shell.execute_reply":"2025-11-17T17:09:38.149731Z"},"papermill":{"duration":0.031429,"end_time":"2025-11-17T17:09:38.15162","exception":false,"start_time":"2025-11-17T17:09:38.120191","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model Architecture","metadata":{"papermill":{"duration":0.021784,"end_time":"2025-11-17T17:09:38.19557","exception":false,"start_time":"2025-11-17T17:09:38.173786","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def build_model():\n    \"\"\"Build 3D U-Net model.\"\"\"\n    model = UNet(\n        spatial_dims=cfg.SPATIAL_DIMS,\n        in_channels=cfg.IN_CHANNELS,\n        out_channels=cfg.OUT_CHANNELS,\n        channels=cfg.CHANNELS,\n        strides=cfg.STRIDES,\n        num_res_units=2,\n    )\n    return model\n\nmodel = build_model().to(cfg.DEVICE)\nprint(f'Model parameters: {sum(p.numel() for p in model.parameters()):,}')","metadata":{"execution":{"iopub.execute_input":"2025-11-17T17:09:38.240541Z","iopub.status.busy":"2025-11-17T17:09:38.239764Z","iopub.status.idle":"2025-11-17T17:09:38.445301Z","shell.execute_reply":"2025-11-17T17:09:38.444426Z"},"papermill":{"duration":0.229543,"end_time":"2025-11-17T17:09:38.446624","exception":false,"start_time":"2025-11-17T17:09:38.217081","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Loss Functions","metadata":{"papermill":{"duration":0.024481,"end_time":"2025-11-17T17:09:38.494114","exception":false,"start_time":"2025-11-17T17:09:38.469633","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class TopologyAwareLoss(nn.Module):\n    \"\"\"Combined loss for topology preservation.\"\"\"\n    def __init__(self):\n        super().__init__()\n        self.dice_loss = DiceLoss(sigmoid=True)\n        self.bce_loss = nn.BCEWithLogitsLoss()\n        \n    def forward(self, pred, target):\n        dice = self.dice_loss(pred, target)\n        bce = self.bce_loss(pred, target)\n        return 0.5 * dice + 0.5 * bce\n\ncriterion = TopologyAwareLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=cfg.LR, weight_decay=cfg.WEIGHT_DECAY)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=cfg.EPOCHS)","metadata":{"execution":{"iopub.execute_input":"2025-11-17T17:09:38.540144Z","iopub.status.busy":"2025-11-17T17:09:38.539534Z","iopub.status.idle":"2025-11-17T17:09:38.545239Z","shell.execute_reply":"2025-11-17T17:09:38.544712Z"},"papermill":{"duration":0.02981,"end_time":"2025-11-17T17:09:38.546304","exception":false,"start_time":"2025-11-17T17:09:38.516494","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training Loop","metadata":{"papermill":{"duration":0.022028,"end_time":"2025-11-17T17:09:38.590499","exception":false,"start_time":"2025-11-17T17:09:38.568471","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def train_epoch(model, loader, criterion, optimizer, scaler, device):\n    model.train()\n    total_loss = 0\n    \n    pbar = tqdm(loader, desc='Training')\n    for imgs, labels in pbar:\n        imgs, labels = imgs.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        \n        if cfg.MIXED_PRECISION:\n            with torch.cuda.amp.autocast():\n                preds = model(imgs)\n                loss = criterion(preds, labels)\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n        else:\n            preds = model(imgs)\n            loss = criterion(preds, labels)\n            loss.backward()\n            optimizer.step()\n        \n        total_loss += loss.item()\n        pbar.set_postfix({'loss': f'{loss.item():.4f}'})\n    \n    return total_loss / len(loader)\n\ndef validate(model, loader, criterion, device):\n    model.eval()\n    total_loss = 0\n    \n    with torch.no_grad():\n        for imgs, labels in tqdm(loader, desc='Validation'):\n            imgs, labels = imgs.to(device), labels.to(device)\n            preds = model(imgs)\n            loss = criterion(preds, labels)\n            total_loss += loss.item()\n    \n    return total_loss / len(loader)","metadata":{"execution":{"iopub.execute_input":"2025-11-17T17:09:38.635285Z","iopub.status.busy":"2025-11-17T17:09:38.635085Z","iopub.status.idle":"2025-11-17T17:09:38.641341Z","shell.execute_reply":"2025-11-17T17:09:38.640823Z"},"papermill":{"duration":0.030184,"end_time":"2025-11-17T17:09:38.64245","exception":false,"start_time":"2025-11-17T17:09:38.612266","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load Data and Train","metadata":{"papermill":{"duration":0.02208,"end_time":"2025-11-17T17:09:38.686934","exception":false,"start_time":"2025-11-17T17:09:38.664854","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Load training data\ntrain_df = pd.read_csv(cfg.TRAIN_CSV)\nprint(f'Training samples: {len(train_df)}')\n\n# Create train/validation split\ntrain_df, val_df = train_test_split(train_df, test_size=0.2, random_state=42)\nprint(f'Train: {len(train_df)}, Validation: {len(val_df)}')\n\n# Create dataset and loader\ntrain_dataset = VesuviusDataset(\n    train_df, \n    cfg.TRAIN_DIR, \n    cfg.TRAIN_LABELS,\n    transforms=train_transforms,\n    is_train=True\n)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=cfg.BATCH_SIZE,\n    shuffle=True,\n    num_workers=cfg.NUM_WORKERS,\n    pin_memory=True\n)\n\n# Create validation dataset and loader\nval_dataset = VesuviusDataset(\n    val_df,\n    cfg.TRAIN_DIR,\n    cfg.TRAIN_LABELS,\n    transforms=None,  # No augmentation for validation\n    is_train=True\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=cfg.BATCH_SIZE,\n    shuffle=False,\n    num_workers=cfg.NUM_WORKERS,\n    pin_memory=True\n)\n\n# Training\nscaler = torch.cuda.amp.GradScaler() if cfg.MIXED_PRECISION else None\nbest_loss = float('inf')\n\nfor epoch in range(cfg.EPOCHS):\n    print(f'\\nEpoch {epoch+1}/{cfg.EPOCHS}')\n    \n    train_loss = train_epoch(model, train_loader, criterion, optimizer, scaler, cfg.DEVICE)\n    print(f'Train Loss: {train_loss:.4f}')\n        val_loss = validate(model, val_loader, criterion, cfg.DEVICE)\n    print(f'Val Loss: {val_loss:.4f}')\n    \n    scheduler.step()\n    \n    # Save best model\n            best_loss = val_loss        torch.save(model.state_dict(), cfg.OUTPUT_DIR / 'best_model.pth')\n        print(f'✓ Model saved (loss: {best_loss:.4f})')\n\nprint('\\n✅ Training complete!')","metadata":{"execution":{"iopub.execute_input":"2025-11-17T17:09:38.733819Z","iopub.status.busy":"2025-11-17T17:09:38.733247Z","iopub.status.idle":"2025-11-17T17:09:38.994677Z","shell.execute_reply":"2025-11-17T17:09:38.993524Z"},"papermill":{"duration":0.286616,"end_time":"2025-11-17T17:09:38.995838","exception":true,"start_time":"2025-11-17T17:09:38.709222","status":"failed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load best model\nmodel.load_state_dict(torch.load(cfg.OUTPUT_DIR / 'best_model.pth'))\nmodel.eval()\n\n# Load test data\ntest_df = pd.read_csv(cfg.TEST_CSV)\ntest_dataset = VesuviusDataset(test_df, cfg.TEST_DIR, is_train=False)\ntest_loader = DataLoader(test_dataset, batch_size=1, num_workers=cfg.NUM_WORKERS)\n\nprint(f'Generating predictions for {len(test_df)} volumes...')\n\n# Create submission\nsubmission_path = cfg.OUTPUT_DIR / 'submission.zip'\nwith zipfile.ZipFile(submission_path, 'w', zipfile.ZIP_DEFLATED) as zf:\n    for img, vol_id in tqdm(test_loader, desc='Inference'):\n        img = img.to(cfg.DEVICE)\n        \n        with torch.no_grad():\n            pred = model(img)\n            pred = torch.sigmoid(pred)\n            pred = (pred > 0.5).cpu().numpy()[0, 0].astype(np.uint8)\n        \n        # Save as TIF\n        frames = [Image.fromarray(pred[i]) for i in range(pred.shape[0])]\n        buf = BytesIO()\n        frames[0].save(buf, format='TIFF', save_all=True, \n                      append_images=frames[1:], compression='tiff_deflate')\n        zf.writestr(f'{vol_id[0]}.tif', buf.getvalue())\n\nprint(f'\\n✅ Submission ready: {submission_path}')\nprint('\\n🏆 Submit this file to the competition!')","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference and Submission","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[]}}]}