{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":"gpu","dataSources":[{"sourceId":117682,"databundleVersionId":15062069,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":704657,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":504433,"modelId":519418}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# CELL 1: Imports\nimport 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\n\nfrom PIL import Image\nimport tifffile\nfrom skimage import morphology, measure\nfrom scipy import ndimage\n\nimport matplotlib.pyplot as plt\nfrom tqdm.auto import tqdm\nfrom pathlib import Path\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport os\nimport gc\nimport zipfile\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Device: {device}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:24.706589Z","iopub.execute_input":"2025-12-31T08:09:24.707379Z","iopub.status.idle":"2025-12-31T08:09:30.689712Z","shell.execute_reply.started":"2025-12-31T08:09:24.707349Z","shell.execute_reply":"2025-12-31T08:09:30.688900Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 2: Configuration\nclass Config:\n    # Data paths\n    DATA_ROOT = Path('/kaggle/input/vesuvius-challenge-surface-detection')\n    TEST_CSV = DATA_ROOT / 'test.csv'\n    TEST_IMAGES = DATA_ROOT / 'test_images'\n    MODEL_PATH = '/kaggle/input/unet2-5segment/pytorch/default/4/best_model.pth'\n    \n    # Model architecture\n    IN_CHANNELS_2_5D = 7\n    NUM_CLASSES = 3\n    BASE_FEATURES = 24\n    PATCH_SIZE = 128\n    \n    # Inference settings\n    BATCH_SIZE = 64\n    NUM_WORKERS = 0\n    OVERLAP = 0.25\n    \n    # Output\n    OUTPUT_DIR = Path('/kaggle/working/submission_tifs')\n    SUBMISSION_ZIP = 'submission.zip'\n\ncfg = Config()\ncfg.OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\nprint('✓ Configuration loaded')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:30.691074Z","iopub.execute_input":"2025-12-31T08:09:30.691515Z","iopub.status.idle":"2025-12-31T08:09:30.697040Z","shell.execute_reply.started":"2025-12-31T08:09:30.691496Z","shell.execute_reply":"2025-12-31T08:09:30.696289Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 3: Data utilities\ndef load_3d_tiff(filepath):\n    \"\"\"Load 3D TIFF volume\"\"\"\n    try:\n        with Image.open(filepath) as img:\n            frames = []\n            try:\n                while True:\n                    frames.append(np.array(img.copy()))\n                    img.seek(img.tell() + 1)\n            except EOFError:\n                pass\n            if frames:\n                return np.stack(frames, axis=0)\n    except Exception as e:\n        print(f\"Error loading {filepath}: {e}\")\n        return None\n\ndef normalize_volume(volume):\n    \"\"\"Normalize volume intensities\"\"\"\n    p_low, p_high = np.percentile(volume, [1, 99])\n    volume = np.clip(volume, p_low, p_high)\n    volume = (volume - p_low) / (p_high - p_low + 1e-8)\n    return volume.astype(np.float32)\n\nprint('✓ Data utilities loaded')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:30.697762Z","iopub.execute_input":"2025-12-31T08:09:30.697967Z","iopub.status.idle":"2025-12-31T08:09:30.712762Z","shell.execute_reply.started":"2025-12-31T08:09:30.697939Z","shell.execute_reply":"2025-12-31T08:09:30.712144Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 4: Model Architecture\nclass AttentionBlock(nn.Module):\n    def __init__(self, channels):\n        super().__init__()\n        self.query = nn.Conv2d(channels, channels // 8, 1)\n        self.key = nn.Conv2d(channels, channels // 8, 1)\n        self.value = nn.Conv2d(channels, channels, 1)\n        self.gamma = nn.Parameter(torch.zeros(1))\n    \n    def forward(self, x):\n        B, C, H, W = x.shape\n        q = self.query(x).view(B, -1, H * W).permute(0, 2, 1)\n        k = self.key(x).view(B, -1, H * W)\n        v = self.value(x).view(B, -1, H * W)\n        attention = F.softmax(torch.bmm(q, k), dim=-1)\n        out = torch.bmm(v, attention.permute(0, 2, 1))\n        out = out.view(B, C, H, W)\n        return self.gamma * out + x\n\nclass ConvBlock2D(nn.Module):\n    def __init__(self, in_ch, out_ch, use_attention=False):\n        super().__init__()\n        self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(out_ch)\n        self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(out_ch)\n        self.relu = nn.ReLU(inplace=True)\n        self.attention = AttentionBlock(out_ch) if use_attention else None\n        self.residual = nn.Conv2d(in_ch, out_ch, 1) if in_ch != out_ch else nn.Identity()\n    \n    def forward(self, x):\n        residual = self.residual(x)\n        out = self.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        if self.attention is not None:\n            out = self.attention(out)\n        out += residual\n        return self.relu(out)\n\nclass UNet2_5D_Advanced(nn.Module):\n    def __init__(self, in_channels=7, num_classes=3, base_features=24):\n        super().__init__()\n        self.enc1 = ConvBlock2D(in_channels, base_features)\n        self.pool1 = nn.MaxPool2d(2)\n        self.enc2 = ConvBlock2D(base_features, base_features * 2)\n        self.pool2 = nn.MaxPool2d(2)\n        self.enc3 = ConvBlock2D(base_features * 2, base_features * 4)\n        self.pool3 = nn.MaxPool2d(2)\n        self.enc4 = ConvBlock2D(base_features * 4, base_features * 8, use_attention=True)\n        self.pool4 = nn.MaxPool2d(2)\n        self.bottleneck = ConvBlock2D(base_features * 8, base_features * 16, use_attention=True)\n        self.up4 = nn.ConvTranspose2d(base_features * 16, base_features * 8, 2, stride=2)\n        self.dec4 = ConvBlock2D(base_features * 16, base_features * 8)\n        self.up3 = nn.ConvTranspose2d(base_features * 8, base_features * 4, 2, stride=2)\n        self.dec3 = ConvBlock2D(base_features * 8, base_features * 4)\n        self.up2 = nn.ConvTranspose2d(base_features * 4, base_features * 2, 2, stride=2)\n        self.dec2 = ConvBlock2D(base_features * 4, base_features * 2)\n        self.up1 = nn.ConvTranspose2d(base_features * 2, base_features, 2, stride=2)\n        self.dec1 = ConvBlock2D(base_features * 2, base_features)\n        self.out = nn.Conv2d(base_features, num_classes, 1)\n        self.out_deep3 = nn.Conv2d(base_features * 4, num_classes, 1)\n        self.out_deep2 = nn.Conv2d(base_features * 2, num_classes, 1)\n    \n    def forward(self, x):\n        enc1 = self.enc1(x)\n        enc2 = self.enc2(self.pool1(enc1))\n        enc3 = self.enc3(self.pool2(enc2))\n        enc4 = self.enc4(self.pool3(enc3))\n        bottleneck = self.bottleneck(self.pool4(enc4))\n        dec4 = self.up4(bottleneck)\n        dec4 = torch.cat([dec4, enc4], dim=1)\n        dec4 = self.dec4(dec4)\n        dec3 = self.up3(dec4)\n        dec3 = torch.cat([dec3, enc3], dim=1)\n        dec3 = self.dec3(dec3)\n        dec2 = self.up2(dec3)\n        dec2 = torch.cat([dec2, enc2], dim=1)\n        dec2 = self.dec2(dec2)\n        dec1 = self.up1(dec2)\n        dec1 = torch.cat([dec1, enc1], dim=1)\n        dec1 = self.dec1(dec1)\n        return self.out(dec1)\n\nprint('✓ Model architecture loaded')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:30.714668Z","iopub.execute_input":"2025-12-31T08:09:30.714942Z","iopub.status.idle":"2025-12-31T08:09:30.733423Z","shell.execute_reply.started":"2025-12-31T08:09:30.714925Z","shell.execute_reply":"2025-12-31T08:09:30.732788Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 5: Load Model\nprint(\"Loading model...\")\nmodel = UNet2_5D_Advanced(\n    in_channels=cfg.IN_CHANNELS_2_5D,\n    num_classes=cfg.NUM_CLASSES,\n    base_features=cfg.BASE_FEATURES\n).to(device)\n\ncheckpoint = torch.load(cfg.MODEL_PATH, map_location=device, weights_only=False)\nmodel.load_state_dict(checkpoint['model_state_dict'])\nmodel.eval()\n\nprint(f\"✓ Model loaded (epoch {checkpoint.get('epoch', 'N/A')})\")\nprint(f\"  Val loss: {checkpoint.get('val_loss', 'N/A')}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:30.734037Z","iopub.execute_input":"2025-12-31T08:09:30.734309Z","iopub.status.idle":"2025-12-31T08:09:32.296258Z","shell.execute_reply.started":"2025-12-31T08:09:30.734291Z","shell.execute_reply":"2025-12-31T08:09:32.295621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 6: Inference Dataset\nclass InferenceDataset(Dataset):\n    def __init__(self, volume_id, volume_path, patch_size=128, num_slices=7, overlap=0.25):\n        self.volume_id = volume_id\n        self.patch_size = patch_size\n        self.num_slices = num_slices\n        \n        print(f\"  Loading: {volume_id}\")\n        self.volume = load_3d_tiff(volume_path)\n        self.volume = normalize_volume(self.volume)\n        self.D, self.H, self.W = self.volume.shape\n        print(f\"    Shape: {self.volume.shape}\")\n        \n        self.patches = self._generate_patches(overlap)\n        print(f\"    Patches: {len(self.patches)}\")\n    \n    def _generate_patches(self, overlap):\n        patches = []\n        stride = int(self.patch_size * (1 - overlap))\n        half = self.num_slices // 2\n        \n        for z in range(half, self.D - half):\n            for y in range(0, self.H, stride):\n                for x in range(0, self.W, stride):\n                    patches.append({\n                        'z_center': z,\n                        'y_start': y,\n                        'x_start': x,\n                        'y_end': min(y + self.patch_size, self.H),\n                        'x_end': min(x + self.patch_size, self.W)\n                    })\n        return patches\n    \n    def __len__(self):\n        return len(self.patches)\n    \n    def __getitem__(self, idx):\n        coord = self.patches[idx]\n        z = coord['z_center']\n        half = self.num_slices // 2\n        \n        patch = self.volume[\n            z - half:z + half + 1,\n            coord['y_start']:coord['y_end'],\n            coord['x_start']:coord['x_end']\n        ]\n        \n        if patch.shape[1] < self.patch_size or patch.shape[2] < self.patch_size:\n            pad_h = self.patch_size - patch.shape[1]\n            pad_w = self.patch_size - patch.shape[2]\n            patch = np.pad(patch, ((0, 0), (0, pad_h), (0, pad_w)), mode='reflect')\n        \n        patch = torch.from_numpy(patch.copy()).float()\n        return patch, coord\n\nprint('✓ Dataset class loaded')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:32.296947Z","iopub.execute_input":"2025-12-31T08:09:32.297203Z","iopub.status.idle":"2025-12-31T08:09:32.305920Z","shell.execute_reply.started":"2025-12-31T08:09:32.297177Z","shell.execute_reply":"2025-12-31T08:09:32.305304Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 7: Prediction Function\n@torch.no_grad()\ndef predict_volume(volume_id, volume_path):\n    \"\"\"Predict segmentation for volume\"\"\"\n    dataset = InferenceDataset(\n        volume_id, volume_path,\n        patch_size=cfg.PATCH_SIZE,\n        num_slices=cfg.IN_CHANNELS_2_5D,\n        overlap=cfg.OVERLAP\n    )\n    \n    loader = DataLoader(\n        dataset,\n        batch_size=cfg.BATCH_SIZE,\n        shuffle=False,\n        num_workers=cfg.NUM_WORKERS\n    )\n    \n    D, H, W = dataset.D, dataset.H, dataset.W\n    pred_sum = np.zeros((D, H, W, cfg.NUM_CLASSES), dtype=np.float32)\n    pred_count = np.zeros((D, H, W), dtype=np.float32)\n    \n    print(\"    Predicting...\")\n    for patches, coords in tqdm(loader, desc=\"    Batches\", leave=False):\n        patches = patches.to(device)\n        outputs = model(patches)\n        probs = F.softmax(outputs, dim=1).cpu().numpy()\n        \n        for i in range(len(patches)):\n            z = coords['z_center'][i].item()\n            y_s = coords['y_start'][i].item()\n            y_e = coords['y_end'][i].item()\n            x_s = coords['x_start'][i].item()\n            x_e = coords['x_end'][i].item()\n            \n            h = y_e - y_s\n            w = x_e - x_s\n            \n            pred_sum[z, y_s:y_e, x_s:x_e] += probs[i, :, :h, :w].transpose(1, 2, 0)\n            pred_count[z, y_s:y_e, x_s:x_e] += 1\n    \n    pred_count = np.maximum(pred_count, 1)\n    pred_probs = pred_sum / pred_count[..., np.newaxis]\n    pred_volume = np.argmax(pred_probs, axis=-1).astype(np.uint8)\n    \n    del dataset, loader, pred_sum, pred_count, pred_probs\n    gc.collect()\n    \n    print(f\"    ✓ Predicted: {pred_volume.shape}\")\n    return pred_volume\n\nprint('✓ Prediction function loaded')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:32.306582Z","iopub.execute_input":"2025-12-31T08:09:32.306826Z","iopub.status.idle":"2025-12-31T08:09:32.326713Z","shell.execute_reply.started":"2025-12-31T08:09:32.306800Z","shell.execute_reply":"2025-12-31T08:09:32.326145Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 8: ⭐ CRITICAL - Simplification for Scorer ⭐\ndef make_scoreable(pred_volume):\n    \"\"\"\n    CRITICAL: Simplify predictions to prevent scorer hangs\n    Keeps ML quality but makes topology scoreable\n    \"\"\"\n    print(\"    Simplifying for scorer...\")\n    \n    mask = pred_volume.astype(bool)\n    original_fg = mask.sum() / mask.size * 100\n    \n    # Step 1: Remove tiny components\n    mask = morphology.remove_small_objects(mask, min_size=3000, connectivity=3)\n    \n    # Step 2: Limit to top 10 components (prevents \"too many objects\" hang)\n    labeled = measure.label(mask, connectivity=3)\n    n_components = labeled.max()\n    \n    if n_components > 10:\n        sizes = ndimage.sum(mask, labeled, range(1, n_components + 1))\n        top_10 = np.argsort(sizes)[-10:] + 1\n        mask = np.isin(labeled, top_10)\n        print(f\"      Reduced {n_components} → 10 components\")\n    \n    # Step 3: Smooth surfaces\n    kernel = morphology.ball(2)\n    mask = morphology.binary_closing(mask, footprint=kernel)\n    mask = morphology.binary_opening(mask, footprint=kernel)\n    \n    # Step 4: Fill holes (prevents complex topology)\n    mask = morphology.remove_small_holes(mask, area_threshold=5000)\n    \n    final_fg = mask.sum() / mask.size * 100\n    final_components = measure.label(mask).max()\n    \n    print(f\"      Foreground: {original_fg:.1f}% → {final_fg:.1f}%\")\n    print(f\"      Components: {final_components}\")\n    \n    return mask.astype(np.uint8)\n\nprint('✓ Simplification function loaded')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:32.327445Z","iopub.execute_input":"2025-12-31T08:09:32.327701Z","iopub.status.idle":"2025-12-31T08:09:32.344370Z","shell.execute_reply.started":"2025-12-31T08:09:32.327678Z","shell.execute_reply":"2025-12-31T08:09:32.343684Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 9: Main Processing Loop\nprint(\"\\n\" + \"=\"*80)\nprint(\"GENERATING PREDICTIONS\")\nprint(\"=\"*80)\n\ntest_df = pd.read_csv(cfg.TEST_CSV)\nprint(f\"\\nTest volumes: {len(test_df)}\\n\")\n\nprocessed_count = 0\n\nfor idx, row in test_df.iterrows():\n    volume_id = row['id']\n    filename = f\"{volume_id}.tif\"\n    volume_path = cfg.TEST_IMAGES / filename\n    \n    print(f\"[{idx+1}/{len(test_df)}] Processing: {filename}\")\n    \n    if not volume_path.exists():\n        print(f\"  ⚠ File not found: {volume_path}\")\n        continue\n    \n    try:\n        # Step 1: ML Model Prediction\n        pred_volume = predict_volume(volume_id, str(volume_path))\n        \n        # Step 2: ⭐ CRITICAL - Simplify for Scorer ⭐\n        pred_volume = make_scoreable(pred_volume)\n        \n        # Step 3: Save\n        output_path = cfg.OUTPUT_DIR / filename\n        tifffile.imwrite(str(output_path), pred_volume)\n        \n        # Verify\n        size_mb = output_path.stat().st_size / 1024**2\n        print(f\"    ✓ Saved: {size_mb:.1f} MB\")\n        \n        processed_count += 1\n        \n    except Exception as e:\n        print(f\"    ✗ Error: {e}\")\n        import traceback\n        traceback.print_exc()\n        continue\n    \n    print()\n\nprint(\"=\"*80)\nprint(f\"✓ Processed: {processed_count}/{len(test_df)} volumes\")\nprint(\"=\"*80)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:32.345056Z","iopub.execute_input":"2025-12-31T08:09:32.345326Z","iopub.status.idle":"2025-12-31T08:09:49.043680Z","shell.execute_reply.started":"2025-12-31T08:09:32.345295Z","shell.execute_reply":"2025-12-31T08:09:49.042887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 10: Create Submission Zip (Using Leaderboard Method)\nprint(\"\\n\" + \"=\"*80)\nprint(\"CREATING SUBMISSION\")\nprint(\"=\"*80)\n\n# Zip all .tif files for submission\nif processed_count > 0:\n    tif_dir = cfg.OUTPUT_DIR\n    tif_files = sorted(tif_dir.glob(\"*.tif\"))\n    zip_path = cfg.SUBMISSION_ZIP\n    \n    with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:\n        for tif_file in tif_files:\n            zipf.write(tif_file, tif_file.name)\n            print(f\"  Added: {tif_file.name}\")\n    \n    # Verify\n    zip_size = Path(zip_path).stat().st_size / 1024**2\n    \n    print(\"\\n\" + \"=\"*80)\n    print(\"SUBMISSION READY\")\n    print(\"=\"*80)\n    print(f\"Files: {len(tif_files)} volumes\")\n    print(f\"Zip: {zip_path} ({zip_size:.1f} MB)\")\n    print(\"=\"*80)\nelse:\n    print(\"\\n⚠ No predictions to zip.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:49.045368Z","iopub.execute_input":"2025-12-31T08:09:49.045672Z","iopub.status.idle":"2025-12-31T08:09:49.254869Z","shell.execute_reply.started":"2025-12-31T08:09:49.045653Z","shell.execute_reply":"2025-12-31T08:09:49.254088Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 11: Verify Submission\nprint(\"\\nVerifying submission contents...\")\nprint(\"=\"*60)\n\nwith zipfile.ZipFile(cfg.SUBMISSION_ZIP, 'r') as z:\n    for info in z.filelist:\n        compressed_mb = info.compress_size / 1024**2\n        uncompressed_mb = info.file_size / 1024**2\n        ratio = (1 - info.compress_size / info.file_size) * 100 if info.file_size > 0 else 0\n        print(f\"{info.filename}:\")\n        print(f\"  Compressed:   {compressed_mb:.2f} MB\")\n        print(f\"  Uncompressed: {uncompressed_mb:.2f} MB\")\n        print(f\"  Ratio:        {ratio:.1f}% reduction\")\n\nprint(\"=\"*60)\nprint(\"✓ Submission verified and ready to upload!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:49.255812Z","iopub.execute_input":"2025-12-31T08:09:49.256090Z","iopub.status.idle":"2025-12-31T08:09:49.263750Z","shell.execute_reply.started":"2025-12-31T08:09:49.256064Z","shell.execute_reply":"2025-12-31T08:09:49.262872Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 12: Visualization (Optional)\nprint(\"\\nGenerating preview visualization...\")\n\nif processed_count > 0:\n    # Load first prediction\n    tif_files = sorted(cfg.OUTPUT_DIR.glob(\"*.tif\"))\n    sample_pred = tifffile.imread(str(tif_files[0]))\n    \n    fig, axes = plt.subplots(2, 3, figsize=(15, 10))\n    \n    z_indices = [\n        sample_pred.shape[0] // 4,\n        sample_pred.shape[0] // 2,\n        3 * sample_pred.shape[0] // 4\n    ]\n    \n    for i, z in enumerate(z_indices):\n        # Raw prediction\n        axes[0, i].imshow(sample_pred[z], cmap='gray', vmin=0, vmax=1)\n        axes[0, i].set_title(f'Slice {z}')\n        axes[0, i].axis('off')\n        \n        # Overlay\n        overlay = np.zeros((*sample_pred[z].shape, 3))\n        overlay[sample_pred[z] == 1] = [0, 1, 0]\n        axes[1, i].imshow(overlay)\n        axes[1, i].set_title(f'Papyrus Overlay {z}')\n        axes[1, i].axis('off')\n    \n    plt.tight_layout()\n    plt.savefig('prediction_preview.png', dpi=100, bbox_inches='tight')\n    print(\"✓ Saved: prediction_preview.png\")\n    plt.show()\n    \n    # Stats\n    print(f\"\\nPrediction Statistics:\")\n    print(f\"  Shape: {sample_pred.shape}\")\n    print(f\"  Background: {(sample_pred == 0).sum() / sample_pred.size * 100:.1f}%\")\n    print(f\"  Papyrus:    {(sample_pred == 1).sum() / sample_pred.size * 100:.1f}%\")\n    print(f\"  Components: {measure.label(sample_pred).max()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-31T08:09:49.264493Z","iopub.execute_input":"2025-12-31T08:09:49.264746Z","iopub.status.idle":"2025-12-31T08:09:50.838286Z","shell.execute_reply.started":"2025-12-31T08:09:49.264729Z","shell.execute_reply":"2025-12-31T08:09:50.837590Z"}},"outputs":[],"execution_count":null}]}