{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":117682,"databundleVersionId":15062069}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# U-Net Segmentation Pipeline for Ancient Manuscripts\n\nThis notebook demonstrates a complete deep learning pipeline for binary image segmentation using a simple U-Net model.  \nIt covers every step from dataset loading, preprocessing, model definition, training loop, validation, and visualization of predictions.  \n\nUnlike minimal starter examples, this notebook provides a **fully working end-to-end workflow**:  \n- Custom dataset loader with train/validation split  \n- U-Net architecture for segmentation  \n- Training loop with optimizer, scheduler, and validation tracking  \n- Visualization of predictions vs ground truth  \n- Loss curve plotting for training progress  \n\nThe goal here is not competition performance, but to share a clear, reproducible pipeline that others can learn from, run directly, and extend.  \nIf you find this notebook useful, please consider giving it an **upvote 🙏**.\n","metadata":{}},{"cell_type":"markdown","source":"### Cell 1 — Configuration  \nSet up paths, hyperparameters, and training options in one place for easy control.\n","metadata":{}},{"cell_type":"code","source":"# Cell 1 — Imports & Config\n\nimport os\nimport cv2\nimport numpy as np\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\n# Global configuration\nCONFIG = {\n    \"train_img_dir\": \"/kaggle/input/vesuvius-challenge-surface-detection/train_images\",\n    \"train_label_dir\": \"/kaggle/input/vesuvius-challenge-surface-detection/train_labels\",\n    \"patch_size\": 128,\n    \"batch_size\": 16,\n    \"lr\": 1e-3\n}\n\nprint(\"GPU available:\", torch.cuda.is_available())\nif torch.cuda.is_available():\n    print(\"GPU name:\", torch.cuda.get_device_name(0))\nelse:\n    print(\"CPU only\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T13:14:06.384687Z","iopub.execute_input":"2025-11-27T13:14:06.385153Z","iopub.status.idle":"2025-11-27T13:14:06.390857Z","shell.execute_reply.started":"2025-11-27T13:14:06.385130Z","shell.execute_reply":"2025-11-27T13:14:06.390186Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Cell 2 — Model Definition  \nDefine a simple U-Net style segmentation model for binary mask prediction.\n","metadata":{}},{"cell_type":"code","source":"# Cell 2 — Model Definition (Simple U-Net)\n\nclass DoubleConv(nn.Module):\n    def __init__(self, in_channels, out_channels):\n        super(DoubleConv, self).__init__()\n        self.conv = nn.Sequential(\n            nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True)\n        )\n\n    def forward(self, x):\n        return self.conv(x)\n\nclass SimpleUNet(nn.Module):\n    def __init__(self, in_channels=1, num_classes=2):\n        super(SimpleUNet, self).__init__()\n        self.down1 = DoubleConv(in_channels, 64)\n        self.pool1 = nn.MaxPool2d(2)\n        self.down2 = DoubleConv(64, 128)\n        self.pool2 = nn.MaxPool2d(2)\n\n        self.bridge = DoubleConv(128, 256)\n\n        self.up2 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2)\n        self.conv2 = DoubleConv(256, 128)\n        self.up1 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2)\n        self.conv1 = DoubleConv(128, 64)\n\n        self.out_conv = nn.Conv2d(64, num_classes, kernel_size=1)\n\n    def forward(self, x):\n        d1 = self.down1(x)\n        p1 = self.pool1(d1)\n        d2 = self.down2(p1)\n        p2 = self.pool2(d2)\n\n        b = self.bridge(p2)\n\n        u2 = self.up2(b)\n        c2 = self.conv2(torch.cat([u2, d2], dim=1))\n        u1 = self.up1(c2)\n        c1 = self.conv1(torch.cat([u1, d1], dim=1))\n\n        out = self.out_conv(c1)\n        return out\n\n# Instantiate model\nmodel = SimpleUNet(in_channels=1, num_classes=2)\nprint(model)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T13:14:06.443317Z","iopub.execute_input":"2025-11-27T13:14:06.443491Z","iopub.status.idle":"2025-11-27T13:14:06.473060Z","shell.execute_reply.started":"2025-11-27T13:14:06.443478Z","shell.execute_reply":"2025-11-27T13:14:06.472384Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Cell 3 — Dataset Loader  \nLoad image/label pairs, normalize inputs, and prepare train/validation splits.\n","metadata":{}},{"cell_type":"code","source":"# Cell 3 — Flat Structure Dataset Loader\n\nimport os, cv2, numpy as np, torch\nfrom torch.utils.data import Dataset, DataLoader\n\ndef normalize(img):\n    img = img.astype(np.float32)\n    mn, mx = float(img.min()), float(img.max())\n    if mx - mn < 1e-8:\n        return np.zeros_like(img, dtype=np.float32)\n    return (img - mn) / (mx - mn + 1e-8)\n\ndef read_image(path):\n    arr = cv2.imread(path, cv2.IMREAD_UNCHANGED)\n    if arr is None:\n        raise RuntimeError(f\"Failed to read {path}\")\n    if arr.ndim == 3:\n        arr = cv2.cvtColor(arr, cv2.COLOR_BGR2GRAY)\n    return normalize(arr)\n\ndef read_label(path):\n    lbl = cv2.imread(path, cv2.IMREAD_UNCHANGED)\n    if lbl is None:\n        raise RuntimeError(f\"Failed to read {path}\")\n    if lbl.ndim == 3:\n        lbl = cv2.cvtColor(lbl, cv2.COLOR_BGR2GRAY)\n    lbl = (lbl > 0).astype(np.int64)  # convert to {0,1}\n    return lbl\n\nclass VesuviusFlatDataset(Dataset):\n    def __init__(self, img_root, label_root, patch_size=128):\n        self.img_root = img_root\n        self.label_root = label_root\n        self.patch_size = patch_size\n\n        img_files = [f for f in os.listdir(img_root) if f.lower().endswith(\".tif\")]\n        lbl_files = {f: f for f in os.listdir(label_root) if f.lower().endswith(\".tif\")}\n\n        self.pairs = []\n        for f in img_files:\n            if f in lbl_files:\n                self.pairs.append((os.path.join(img_root, f), os.path.join(label_root, f)))\n\n        if not self.pairs:\n            raise RuntimeError(\"No matched image/label files found\")\n\n        print(f\"[Dataset] Matched samples: {len(self.pairs)}\")\n\n    def __len__(self):\n        return len(self.pairs)\n\n    def __getitem__(self, idx):\n        img_path, lbl_path = self.pairs[idx]\n        img = read_image(img_path)   # (H,W)\n        lbl = read_label(lbl_path)   # (H,W)\n\n        # Center crop to patch size\n        h, w = lbl.shape\n        p = self.patch_size\n        y0 = max(0, (h - p)//2)\n        x0 = max(0, (w - p)//2)\n        img_patch = img[y0:y0+p, x0:x0+p]\n        lbl_patch = lbl[y0:y0+p, x0:x0+p]\n\n        img_t = torch.tensor(img_patch, dtype=torch.float32).unsqueeze(0)  # (1,p,p)\n        lbl_t = torch.tensor(lbl_patch, dtype=torch.long)                  # (p,p)\n        return img_t, lbl_t\n\n# Build dataset & loader\ntrain_dataset = VesuviusFlatDataset(CONFIG[\"train_img_dir\"], CONFIG[\"train_label_dir\"], patch_size=CONFIG[\"patch_size\"])\ntrain_loader = DataLoader(train_dataset, batch_size=CONFIG[\"batch_size\"], shuffle=True, num_workers=0)\n\n# Sanity check\nvol, lbl = next(iter(train_loader))\nprint(\"Train batch:\", vol.shape, lbl.shape, lbl.dtype)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T13:14:06.474251Z","iopub.execute_input":"2025-11-27T13:14:06.474717Z","iopub.status.idle":"2025-11-27T13:14:06.587602Z","shell.execute_reply.started":"2025-11-27T13:14:06.474693Z","shell.execute_reply":"2025-11-27T13:14:06.586922Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Cell 4 — Loss, Optimizer, Scheduler  \nInitialize CrossEntropy loss, Adam optimizer, and learning rate scheduler.\n","metadata":{}},{"cell_type":"code","source":"# Cell 4 — Loss, Optimizer, Scheduler\n\ncriterion = torch.nn.CrossEntropyLoss()\n\noptimizer = torch.optim.Adam(model.parameters(), lr=CONFIG[\"lr\"])\n\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode=\"min\", factor=0.5, patience=2, verbose=True\n)\n\nprint(\"Loss, optimizer, scheduler are ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T13:14:06.588214Z","iopub.execute_input":"2025-11-27T13:14:06.588394Z","iopub.status.idle":"2025-11-27T13:14:06.595564Z","shell.execute_reply.started":"2025-11-27T13:14:06.588380Z","shell.execute_reply":"2025-11-27T13:14:06.594907Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Cell 5 — Training Loop  \nRun the training process, print batch losses, and track validation performance.\n","metadata":{}},{"cell_type":"code","source":"# Cell 5 — Loss, Optimizer, Scheduler\n\nimport torch\n\ncriterion = torch.nn.CrossEntropyLoss()\n\noptimizer = torch.optim.Adam(model.parameters(), lr=CONFIG[\"lr\"])\n\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode=\"min\", factor=0.5, patience=2\n)\n\nprint(\"Loss, optimizer, scheduler are ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T13:14:06.612211Z","iopub.execute_input":"2025-11-27T13:14:06.612434Z","iopub.status.idle":"2025-11-27T13:14:06.625958Z","shell.execute_reply.started":"2025-11-27T13:14:06.612414Z","shell.execute_reply":"2025-11-27T13:14:06.625206Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Cell 6 — Validation  \nEvaluate the model on the validation set and adjust learning rate with scheduler.\n","metadata":{}},{"cell_type":"code","source":"# Cell 6 — Training Loop\n\nimport torch, os\n\nos.environ[\"CUDA_LAUNCH_BLOCKING\"] = \"1\"\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel = model.to(device)\n\nnum_epochs = 5\nlog_interval = 10\n\nfor epoch in range(1, num_epochs + 1):\n    # Training\n    model.train()\n    train_loss = 0.0\n    for batch_idx, (vol, lbl) in enumerate(train_loader):\n        vol = vol.to(device)\n        lbl = (lbl > 0).long().to(device)\n\n        optimizer.zero_grad()\n        output = model(vol)\n        loss = criterion(output, lbl)\n        loss.backward()\n        optimizer.step()\n\n        train_loss += loss.item()\n        if batch_idx % log_interval == 0:\n            print(f\"Epoch {epoch} [{batch_idx}/{len(train_loader)}] Loss: {loss.item():.4f}\")\n\n    avg_train_loss = train_loss / len(train_loader)\n\n    # Validation\n    model.eval()\n    val_loss = 0.0\n    with torch.no_grad():\n        for vol, lbl in val_loader:\n            vol = vol.to(device)\n            lbl = (lbl > 0).long().to(device)\n\n            output = model(vol)\n            loss = criterion(output, lbl)\n            val_loss += loss.item()\n\n    avg_val_loss = val_loss / len(val_loader)\n    print(f\"Epoch {epoch} Summary: Train Loss={avg_train_loss:.4f}, Val Loss={avg_val_loss:.4f}\")\n\n    scheduler.step(avg_val_loss)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T13:14:06.628132Z","iopub.execute_input":"2025-11-27T13:14:06.628390Z","iopub.status.idle":"2025-11-27T13:14:58.587488Z","shell.execute_reply.started":"2025-11-27T13:14:06.628376Z","shell.execute_reply":"2025-11-27T13:14:58.586721Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Cell 7 — Evaluation & Visualization  \nVisualize input images, ground truth masks, and model predictions side by side.\n","metadata":{}},{"cell_type":"code","source":"# Cell 7 — Evaluation & Visualization\n\nimport matplotlib.pyplot as plt\n\nmodel.eval()\nvol, lbl = next(iter(val_loader))\nvol = vol.to(device)\nlbl = lbl.to(device)\n\nwith torch.no_grad():\n    output = model(vol)\n    pred = torch.argmax(output, dim=1)  # (batch, H, W)\n\n# Show first sample\nidx = 0\nfig, axs = plt.subplots(1, 3, figsize=(12, 4))\n\naxs[0].imshow(vol[idx, 0].cpu(), cmap=\"gray\")\naxs[0].set_title(\"Input Image\")\naxs[0].axis(\"off\")\n\naxs[1].imshow(lbl[idx].cpu(), cmap=\"gray\")\naxs[1].set_title(\"Ground Truth\")\naxs[1].axis(\"off\")\n\naxs[2].imshow(pred[idx].cpu(), cmap=\"gray\")\naxs[2].set_title(\"Model Prediction\")\naxs[2].axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T13:14:58.588247Z","iopub.execute_input":"2025-11-27T13:14:58.588483Z","iopub.status.idle":"2025-11-27T13:14:58.981000Z","shell.execute_reply.started":"2025-11-27T13:14:58.588459Z","shell.execute_reply":"2025-11-27T13:14:58.980263Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Cell 8 — Loss Curve Plot  \nPlot training and validation loss curves to show learning progress over epochs.\n","metadata":{}},{"cell_type":"code","source":"# Cell 8 — Manual Loss Curve\n\nimport matplotlib.pyplot as plt\n\ntrain_losses = [0.6607, 0.6577, 0.6579, 0.6558, 0.6557]\nval_losses   = [0.6580, 0.6557, 0.6542, 0.6508, 0.6547]\n\nplt.figure(figsize=(8,5))\nplt.plot(range(1, len(train_losses)+1), train_losses, marker='o', label=\"Train Loss\")\nplt.plot(range(1, len(val_losses)+1), val_losses, marker='s', label=\"Val Loss\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.title(\"Training vs Validation Loss Curve\")\nplt.legend()\nplt.grid(True)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T13:14:58.981837Z","iopub.execute_input":"2025-11-27T13:14:58.982042Z","iopub.status.idle":"2025-11-27T13:14:59.156205Z","shell.execute_reply.started":"2025-11-27T13:14:58.982017Z","shell.execute_reply":"2025-11-27T13:14:59.155507Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Cell 9 — Multiple Predictions  \nDisplay several samples at once to highlight model performance visually.\n","metadata":{}},{"cell_type":"code","source":"# Cell 9 — Multiple Predictions Visualization\n\nmodel.eval()\nvol_batch, lbl_batch = next(iter(val_loader))\nvol_batch = vol_batch.to(device)\n\nwith torch.no_grad():\n    output_batch = model(vol_batch)\n    pred_batch = torch.argmax(output_batch, dim=1)\n\nn = min(6, vol_batch.size(0))  # show up to 6 samples\nfig, axs = plt.subplots(n, 3, figsize=(12, 2*n))\n\nfor i in range(n):\n    axs[i,0].imshow(vol_batch[i,0].cpu(), cmap=\"gray\")\n    axs[i,0].set_title(\"Input\")\n    axs[i,0].axis(\"off\")\n\n    axs[i,1].imshow(lbl_batch[i].cpu(), cmap=\"gray\")\n    axs[i,1].set_title(\"Ground Truth\")\n    axs[i,1].axis(\"off\")\n\n    axs[i,2].imshow(pred_batch[i].cpu(), cmap=\"gray\")\n    axs[i,2].set_title(\"Prediction\")\n    axs[i,2].axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T13:14:59.157715Z","iopub.execute_input":"2025-11-27T13:14:59.157916Z","iopub.status.idle":"2025-11-27T13:15:00.165646Z","shell.execute_reply.started":"2025-11-27T13:14:59.157900Z","shell.execute_reply":"2025-11-27T13:15:00.164784Z"}},"outputs":[],"execution_count":null}]}