{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":117682,"databundleVersionId":15062069,"sourceType":"competition"},{"sourceId":14567533,"sourceType":"datasetVersion","datasetId":9305069}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<div align=\"center\">\n  <h3> 3D Surface Segmentation with MONAI U-Net</h3>\n  <img src=\"https://raw.githubusercontent.com/Mr-Asan/CycleGAN-Pix2pix/main/x.png\" width=\"800\"/>\n</div>\n","metadata":{}},{"cell_type":"markdown","source":"# 1. Introduction\nIn this study, a UNet-based segmentation model was trained on 3D surface images using the MONAI (Medical Open Network for AI) library, and the inference process was performed. Below, the Data Analysis, Training, and Inference phases of the study are presented with both conceptual explanations and sample code.","metadata":{}},{"cell_type":"code","source":"!pip install /kaggle/input/imagecodes-monai-unet-2/*.whl --no-index\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:24:01.999494Z","iopub.execute_input":"2026-01-22T09:24:02.000331Z","iopub.status.idle":"2026-01-22T09:24:09.745848Z","shell.execute_reply.started":"2026-01-22T09:24:02.000298Z","shell.execute_reply":"2026-01-22T09:24:09.745135Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom glob import glob\nimport tifffile as tiff\nfrom tqdm import tqdm\n\nfrom monai.networks.nets import UNet\n\nimport matplotlib.pyplot as plt\nimport cv2\n\nimport imageio\nfrom mpl_toolkits.mplot3d import Axes3D","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 1. Data Analysis\nImage and mask data provided in TIFF format were first examined in two dimensions (2D). Image-mask fit and structural features were analyzed using individual slices. Then, to better understand the spatial continuity of the masks, the data were visualized in three dimensions (3D); the volumetric structure of the data was evaluated by overlaying mask surfaces onto translucent image layers.","metadata":{}},{"cell_type":"code","source":"def visualize_volume_and_mask(\n    image_tif_path,\n    mask_tif_path,\n    num_slices=25,\n    grid_size=5,\n    resize_to=128\n):\n    volume = tiff.imread(image_tif_path)   # (D,H,W)\n    mask = tiff.imread(mask_tif_path)       # (D,H,W)\n\n    assert volume.shape == mask.shape, \"Image & mask shape mismatch!\"\n\n    D, H, W = volume.shape\n\n    slice_indices = np.linspace(0, D - 1, num_slices, dtype=int)\n\n    fig, axes = plt.subplots(\n        grid_size, grid_size * 2,\n        figsize=(grid_size * 4, grid_size * 2)\n    )\n\n    for i, idx in enumerate(slice_indices):\n        r = i // grid_size\n        c = (i % grid_size) * 2\n\n        # --- IMAGE ---\n        img = volume[idx]\n        img = cv2.resize(img, (resize_to, resize_to))\n        img = (img - img.min()) / (img.max() - img.min() + 1e-6)\n\n        axes[r, c].imshow(img, cmap=\"gray\")\n        axes[r, c].set_title(f\"Img z={idx}\", fontsize=8)\n        axes[r, c].axis(\"off\")\n\n        # --- MASK ---\n        m = mask[idx]\n        m = cv2.resize(\n            m,\n            (resize_to, resize_to),\n            interpolation=cv2.INTER_NEAREST\n        )\n\n        axes[r, c + 1].imshow(m, cmap=\"gray\")\n        axes[r, c + 1].set_title(f\"Mask z={idx}\", fontsize=8)\n        axes[r, c + 1].axis(\"off\")\n\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:24:43.461022Z","iopub.execute_input":"2026-01-22T09:24:43.461722Z","iopub.status.idle":"2026-01-22T09:24:43.469975Z","shell.execute_reply.started":"2026-01-22T09:24:43.461692Z","shell.execute_reply":"2026-01-22T09:24:43.469149Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"visualize_volume_and_mask(\n    image_tif_path=\"/kaggle/input/vesuvius-challenge-surface-detection/train_images/1004283650.tif\",\n    mask_tif_path=\"/kaggle/input/vesuvius-challenge-surface-detection/train_labels/1004283650.tif\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:24:43.472066Z","iopub.execute_input":"2026-01-22T09:24:43.472496Z","iopub.status.idle":"2026-01-22T09:24:47.578868Z","shell.execute_reply.started":"2026-01-22T09:24:43.472462Z","shell.execute_reply":"2026-01-22T09:24:47.577825Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"image_path = \"/kaggle/input/vesuvius-challenge-surface-detection/train_images/1004283650.tif\"\nmask_path  = \"/kaggle/input/vesuvius-challenge-surface-detection/train_labels/1004283650.tif\"\n\nimages = tiff.imread(image_path)   \nmasks  = tiff.imread(mask_path)    \n\nprint(\"Image shape:\", images.shape)\nprint(\"Mask shape :\", masks.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:24:47.580064Z","iopub.execute_input":"2026-01-22T09:24:47.580883Z","iopub.status.idle":"2026-01-22T09:24:47.926497Z","shell.execute_reply.started":"2026-01-22T09:24:47.580853Z","shell.execute_reply":"2026-01-22T09:24:47.925634Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"images = images[:, ::2, ::2]\nmasks  = masks[:, ::2, ::2]\n\nnum_slices = 25\nalpha_img = 0.15\nalpha_mask = 0.9\n\nD, H, W = images.shape\n\nslice_indices = np.linspace(\n    D // 4,\n    3 * D // 4 - 1,\n    num_slices\n).astype(int)\n\nslice_indices = np.clip(slice_indices, 0, D - 1)\n\nview_angles = [0, 90, 180, 270]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:24:47.927641Z","iopub.execute_input":"2026-01-22T09:24:47.927931Z","iopub.status.idle":"2026-01-22T09:24:47.933349Z","shell.execute_reply.started":"2026-01-22T09:24:47.927905Z","shell.execute_reply":"2026-01-22T09:24:47.932579Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig = plt.figure(figsize=(25, 6))\n\nfor i, azim in enumerate(view_angles):\n    ax = fig.add_subplot(1, len(view_angles), i + 1, projection=\"3d\")\n\n    for z in slice_indices:\n        Y, X = np.mgrid[0:H, 0:W]\n        Z = np.ones_like(X) * z\n\n        img_slice = images[z]\n        img_norm = (img_slice - img_slice.min()) / (np.ptp(img_slice) + 1e-6)\n\n        colors_img = plt.cm.gray(img_norm)\n        colors_img[..., -1] = alpha_img\n\n        ax.plot_surface(\n            X, Y, Z,\n            facecolors=colors_img,\n            rstride=4,\n            cstride=4,\n            shade=False\n        )\n        papyrus_mask = masks[z] == 1\n\n        if papyrus_mask.any():\n            ax.scatter(\n                X[papyrus_mask],\n                Y[papyrus_mask],\n                Z[papyrus_mask],\n                c=\"red\",\n                s=3,\n                alpha=alpha_mask\n            )\n\n    ax.set_xlim(0, 256)\n    ax.set_ylim(0, 256)\n    ax.set_zlim(0, D)\n\n    ax.set_xlabel(\"X\")\n    ax.set_ylabel(\"Y\")\n    ax.set_zlabel(\"Slice\")\n\n    ax.view_init(elev=25, azim=azim)\n    ax.set_title(f\"Z rotation {azim}°\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:24:47.934431Z","iopub.execute_input":"2026-01-22T09:24:47.934722Z","iopub.status.idle":"2026-01-22T09:25:26.378277Z","shell.execute_reply.started":"2026-01-22T09:24:47.934697Z","shell.execute_reply":"2026-01-22T09:25:26.377474Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 3.Training\nThe MONAI library's 3D UNet architecture was used in the training phase. The model learns the spatial context between successive slices by taking 3D TIFF volumes as input and distinguishes between papyrus and background classes at the voxel level. Cross-entropy loss and the AdamW optimization method were used in the training process.","metadata":{}},{"cell_type":"code","source":"class Tiff3DDataset(Dataset):\n    def __init__(self, image_paths, mask_paths):\n        self.image_paths = image_paths\n        self.mask_paths = mask_paths\n\n    def __len__(self):\n        return len(self.image_paths)\n\n    def __getitem__(self, idx):\n        img = tiff.imread(self.image_paths[idx])   \n        mask = tiff.imread(self.mask_paths[idx])  \n\n        img = img.astype(np.float32)\n        img = (img - img.min()) / (img.max() - img.min() + 1e-6)\n\n        img = torch.from_numpy(img).unsqueeze(0)   \n        mask = torch.from_numpy(mask).long()       \n\n        return img, mask\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:25:26.379326Z","iopub.execute_input":"2026-01-22T09:25:26.379589Z","iopub.status.idle":"2026-01-22T09:25:26.385727Z","shell.execute_reply.started":"2026-01-22T09:25:26.379564Z","shell.execute_reply":"2026-01-22T09:25:26.385086Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nmodel = UNet(\n    spatial_dims=3,\n    in_channels=1,\n    out_channels=2,                # background / papyrus\n    channels=(32, 64, 128, 256, 512),\n    strides=(2, 2, 2, 2),\n    num_res_units=2,\n).to(device)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:25:26.386649Z","iopub.execute_input":"2026-01-22T09:25:26.386863Z","iopub.status.idle":"2026-01-22T09:25:26.749777Z","shell.execute_reply.started":"2026-01-22T09:25:26.386842Z","shell.execute_reply":"2026-01-22T09:25:26.749159Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"criterion = nn.CrossEntropyLoss(ignore_index=2)\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=1e-4,\n    weight_decay=1e-5\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:25:26.751940Z","iopub.execute_input":"2026-01-22T09:25:26.752206Z","iopub.status.idle":"2026-01-22T09:25:26.757292Z","shell.execute_reply.started":"2026-01-22T09:25:26.752182Z","shell.execute_reply":"2026-01-22T09:25:26.756584Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_images = sorted(glob(\"/kaggle/input/vesuvius-challenge-surface-detection/train_images/*.tif\"))\ntrain_masks  = sorted(glob(\"/kaggle/input/vesuvius-challenge-surface-detection/train_labels/*.tif\"))\n\ntrain_dataset = Tiff3DDataset(train_images, train_masks)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=1,      \n    shuffle=True,\n    num_workers=4,\n    pin_memory=True\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:25:26.758050Z","iopub.execute_input":"2026-01-22T09:25:26.758303Z","iopub.status.idle":"2026-01-22T09:25:26.801618Z","shell.execute_reply.started":"2026-01-22T09:25:26.758281Z","shell.execute_reply":"2026-01-22T09:25:26.801141Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 150\nSAVE_PATH = \"unet3d_papyrus.pth\"\n\nbest_loss = 1e9\n\nfor epoch in range(EPOCHS):\n    model.train()\n    epoch_loss = 0.0\n\n    for imgs, masks in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{EPOCHS}\"):\n        imgs = imgs.to(device, non_blocking=True)\n        masks = masks.to(device, non_blocking=True)\n\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, masks)\n        loss.backward()\n        optimizer.step()\n\n        epoch_loss += loss.item()\n\n    avg_loss = epoch_loss / len(train_loader)\n    print(f\"Epoch {epoch+1} | Loss: {avg_loss:.6f}\")\n\n    if avg_loss < best_loss:\n        best_loss = avg_loss\n        torch.save({\n            \"epoch\": epoch,\n            \"model_state_dict\": model.state_dict(),\n            \"optimizer_state_dict\": optimizer.state_dict(),\n            \"loss\": best_loss\n        }, SAVE_PATH)\n\n        print(f\"✅ Model saved (loss={best_loss:.6f})\")\n#This training code runs flawlessly, utilizing 12GB of GPU power. \n#In this section, the training process was performed on a higher-powered device, saving time. The test section presents outputs from the trained model. \n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 3. Inference\nThe model structure used was visualized, and the testing processes were checked using 2D outputs and 3D spatial visualization of original masks and manufactured synthetic masks.","metadata":{}},{"cell_type":"code","source":"checkpoint = torch.load(\"/kaggle/input/imagecodes-monai-unet-2/unet3d_papyrus.pth\", map_location=device)\nmodel.load_state_dict(checkpoint[\"model_state_dict\"])\nmodel.eval()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:25:26.802377Z","iopub.execute_input":"2026-01-22T09:25:26.802836Z","iopub.status.idle":"2026-01-22T09:25:28.973058Z","shell.execute_reply.started":"2026-01-22T09:25:26.802812Z","shell.execute_reply":"2026-01-22T09:25:28.972314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_3d_volume(model, tif_path):\n    vol = tiff.imread(tif_path)\n    vol = vol.astype(np.float32)\n    vol = (vol - vol.min()) / (vol.max() - vol.min() + 1e-6)\n\n    vol = torch.from_numpy(vol).unsqueeze(0).unsqueeze(0).to(device)\n\n    with torch.no_grad():\n        out = model(vol)\n        pred = torch.argmax(out, dim=1)\n\n    return pred.squeeze(0).cpu().numpy()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:25:28.974222Z","iopub.execute_input":"2026-01-22T09:25:28.974501Z","iopub.status.idle":"2026-01-22T09:25:28.979521Z","shell.execute_reply.started":"2026-01-22T09:25:28.974478Z","shell.execute_reply":"2026-01-22T09:25:28.978865Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_volume = \"/kaggle/input/vesuvius-challenge-surface-detection/test_images/1407735.tif\"\nprediction = predict_3d_volume(model, test_volume)\n\ntiff.imwrite(\n    \"prediction.tif\",\n    prediction.astype(np.uint8)\n)\n\nprint(\"✅ Prediction saved as prediction.tif\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:25:28.980400Z","iopub.execute_input":"2026-01-22T09:25:28.980779Z","iopub.status.idle":"2026-01-22T09:25:30.990815Z","shell.execute_reply.started":"2026-01-22T09:25:28.980756Z","shell.execute_reply":"2026-01-22T09:25:30.990154Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tifffile as tiff\nimport matplotlib.pyplot as plt\nimport numpy as np\n\npred = tiff.imread(\"prediction.tif\")  # (Z, H, W)\n\nnum_slices = pred.shape[0]\n\nindices = np.linspace(0, num_slices - 1, 25, dtype=int)\n\nfig, axes = plt.subplots(5, 5, figsize=(10, 10))\n\nfor ax, idx in zip(axes.flat, indices):\n    ax.imshow(pred[idx], cmap=\"gray\")\n    ax.set_title(f\"Slice {idx}\", fontsize=8)\n    ax.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n#Predicted Masks","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:25:30.991751Z","iopub.execute_input":"2026-01-22T09:25:30.992034Z","iopub.status.idle":"2026-01-22T09:25:32.277007Z","shell.execute_reply.started":"2026-01-22T09:25:30.991999Z","shell.execute_reply":"2026-01-22T09:25:32.276182Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_volume = \"/kaggle/input/vesuvius-challenge-surface-detection/train_images/102536988.tif\"\nprediction = predict_3d_volume(model, test_volume)\n\ntiff.imwrite(\n    \"prediction-2.tif\",\n    prediction.astype(np.uint8)\n)\n\nprint(\"✅ Prediction saved as prediction.tif\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:25:32.278330Z","iopub.execute_input":"2026-01-22T09:25:32.278562Z","iopub.status.idle":"2026-01-22T09:25:34.038452Z","shell.execute_reply.started":"2026-01-22T09:25:32.278533Z","shell.execute_reply":"2026-01-22T09:25:34.037813Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"orig_image_path = \"/kaggle/input/vesuvius-challenge-surface-detection/train_images/102536988.tif\"\norig_mask_path  = \"/kaggle/input/vesuvius-challenge-surface-detection/train_labels/102536988.tif\"\n\npred_image_path = \"/kaggle/input/vesuvius-challenge-surface-detection/train_images/102536988.tif\"\npred_mask_path  = \"prediction-2.tif\"\n\nimages_orig = tiff.imread(orig_image_path)\nmasks_orig  = tiff.imread(orig_mask_path)\n\nimages_pred = tiff.imread(pred_image_path)\nmasks_pred  = tiff.imread(pred_mask_path)\n\nimages_orig = images_orig[:, ::2, ::2]\nmasks_orig  = masks_orig[:, ::2, ::2]\n\nimages_pred = images_pred[:, ::2, ::2]\nmasks_pred  = masks_pred[:, ::2, ::2]\n\nnum_slices = 25\nalpha_img  = 0.15\nalpha_mask = 0.9\n\nview_angles = [0, 90, 180, 270]\n\ndatasets = [\n    (\"ORIGINAL\", images_orig, masks_orig),\n    (\"PREDICTED\", images_pred, masks_pred),\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:30:41.891196Z","iopub.execute_input":"2026-01-22T09:30:41.891565Z","iopub.status.idle":"2026-01-22T09:30:42.494483Z","shell.execute_reply.started":"2026-01-22T09:30:41.891529Z","shell.execute_reply":"2026-01-22T09:30:42.493871Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig = plt.figure(figsize=(26, 12))\n\nfor row, (title_prefix, images, masks) in enumerate(datasets):\n\n    D, H, W = images.shape\n\n    slice_indices = np.linspace(\n        D // 4,\n        3 * D // 4 - 1,\n        num_slices\n    ).astype(int)\n    slice_indices = np.clip(slice_indices, 0, D - 1)\n\n    for col, azim in enumerate(view_angles):\n        ax = fig.add_subplot(\n            len(datasets),\n            len(view_angles),\n            row * len(view_angles) + col + 1,\n            projection=\"3d\"\n        )\n\n        for z in slice_indices:\n            Y, X = np.mgrid[0:H, 0:W]\n            Z = np.ones_like(X) * z\n\n            img_slice = images[z]\n            img_norm = (img_slice - img_slice.min()) / (np.ptp(img_slice) + 1e-6)\n\n            colors_img = plt.cm.gray(img_norm)\n            colors_img[..., -1] = alpha_img\n\n            ax.plot_surface(\n                X, Y, Z,\n                facecolors=colors_img,\n                rstride=4,\n                cstride=4,\n                shade=False\n            )\n\n            papyrus_mask = masks[z] == 1\n            if papyrus_mask.any():\n                ax.scatter(\n                    X[papyrus_mask],\n                    Y[papyrus_mask],\n                    Z[papyrus_mask],\n                    c=\"red\",\n                    s=3,\n                    alpha=alpha_mask\n                )\n\n        ax.set_xlim(0, W)\n        ax.set_ylim(0, H)\n        ax.set_zlim(0, D)\n\n        ax.set_xlabel(\"X\")\n        ax.set_ylabel(\"Y\")\n        ax.set_zlabel(\"Slice\")\n\n        ax.view_init(elev=25, azim=azim)\n        ax.set_title(f\"{title_prefix} | Z {azim}°\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-22T09:30:46.645019Z","iopub.execute_input":"2026-01-22T09:30:46.645662Z","iopub.status.idle":"2026-01-22T09:32:17.692782Z","shell.execute_reply.started":"2026-01-22T09:30:46.645629Z","shell.execute_reply":"2026-01-22T09:32:17.691760Z"}},"outputs":[],"execution_count":null}]}