{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":117682,"databundleVersionId":14443416,"sourceType":"competition"},{"sourceId":674398,"sourceType":"modelInstanceVersion","modelInstanceId":511176,"modelId":525859}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np \nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nimport warnings\nimport pandas as pd\nimport zipfile\nfrom PIL import Image\nfrom scipy.ndimage import binary_erosion, binary_opening, binary_closing\nfrom scipy.ndimage import label as connected_components\nimport matplotlib.pyplot as plt\n\nwarnings.filterwarnings('ignore')\n\n# ============================================================================\n# CONFIGURATION\n# ============================================================================\n\nclass PredictionConfig:\n    \"\"\"Enhanced configuration with post-processing options\"\"\"\n    \n    # Paths - KAGGLE\n    DATA_DIR = \"/kaggle/input/vesuvius-challenge-surface-detection\"\n    MODEL_PATH = \"/kaggle/input/vesuvius-model/pytorch/default/1/final_model_weights_only.pth\"\n    OUTPUT_DIR = \"/kaggle/working/predictions\"\n    SUBMISSION_DIR = \"/kaggle/working\"\n    \n    # Model architecture (MUST MATCH training config!)\n    IN_CHANNELS = 1\n    NUM_CLASSES = 1\n    BASE_FILTERS = 16\n    \n    # Prediction settings\n    BATCH_SIZE = 64\n    PATCH_SIZE = 256\n    STRIDE = 256\n    \n    # Device\n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    # Preprocessing (MUST MATCH training preprocessing!)\n    NORM_METHOD = \"minmax\"\n    CLIP_PERCENTILE = (1, 99)\n    NORMALIZE_PER_VOLUME = False\n    \n    # Thresholding & Post-processing\n    THRESHOLD_STRATEGY = \"adaptive\"  # Options: \"fixed\", \"adaptive\", \"otsu\"\n    FIXED_THRESHOLD = 0.65\n    \n    # Post-processing options\n    USE_POST_PROCESSING = True\n    MORPHOLOGICAL_OPENING = True\n    MORPHOLOGICAL_CLOSING = True\n    EROSION_ITERATIONS = 1\n    OPENING_ITERATIONS = 1\n    CLOSING_ITERATIONS = 1\n    \n    # Component filtering\n    MIN_COMPONENT_SIZE = 50\n    MAX_COMPONENT_SIZE = None\n    \n    # Confidence-based filtering\n    USE_CONFIDENCE_FILTERING = True\n    MIN_CONFIDENCE = 0.6\n    \n    # Test-Time Augmentation (TTA)\n    USE_TTA = False\n    TTA_FLIPS = ['none', 'horizontal', 'vertical']\n\n# ============================================================================\n# TIFF READER\n# ============================================================================\n\nclass SimpleTiffReader:\n    \"\"\"Simple TIFF reader using PIL\"\"\"\n    \n    @staticmethod\n    def read_multipage_tiff(filepath):\n        \"\"\"Read multi-page TIFF file and return as 3D numpy array\"\"\"\n        img = Image.open(filepath)\n        \n        images = []\n        try:\n            for i in range(img.n_frames):\n                img.seek(i)\n                frame = np.array(img)\n                images.append(frame)\n        except EOFError:\n            pass\n        \n        volume = np.stack(images, axis=0)\n        return volume\n    \n    @staticmethod\n    def write_multipage_tiff(filepath, volume, compress=True):\n        \"\"\"Write 3D numpy array as multi-page TIFF\"\"\"\n        images = []\n        for i in range(volume.shape[0]):\n            slice_data = volume[i]\n            if slice_data.dtype != np.uint8:\n                slice_data = slice_data.astype(np.uint8)\n            \n            img = Image.fromarray(slice_data, mode='L')\n            images.append(img)\n        \n        if len(images) > 0:\n            images[0].save(\n                filepath,\n                save_all=True,\n                append_images=images[1:],\n                compression='tiff_deflate' if compress else None\n            )\n\n# ============================================================================\n# MODEL ARCHITECTURE\n# ============================================================================\n\nclass ConvBlock(nn.Module):\n    \"\"\"Optimized double convolution block\"\"\"\n    def __init__(self, in_ch, out_ch):\n        super().__init__()\n        self.conv = nn.Sequential(\n            nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False),\n            nn.BatchNorm2d(out_ch),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False),\n            nn.BatchNorm2d(out_ch),\n            nn.ReLU(inplace=True)\n        )\n    \n    def forward(self, x):\n        return self.conv(x)\n\nclass LightweightUNetPlusPlus(nn.Module):\n    \"\"\"Lightweight U-Net++\"\"\"\n    def __init__(self, in_channels=1, num_classes=1, base_filters=16):\n        super().__init__()\n        \n        filters = [base_filters, base_filters*2, base_filters*4, base_filters*8, base_filters*16]\n        \n        # Encoder\n        self.conv0_0 = ConvBlock(in_channels, filters[0])\n        self.conv1_0 = ConvBlock(filters[0], filters[1])\n        self.conv2_0 = ConvBlock(filters[1], filters[2])\n        self.conv3_0 = ConvBlock(filters[2], filters[3])\n        self.conv4_0 = ConvBlock(filters[3], filters[4])\n        \n        # Nested skip pathways\n        self.conv0_1 = ConvBlock(filters[0] + filters[1], filters[0])\n        self.conv0_2 = ConvBlock(filters[0]*2 + filters[1], filters[0])\n        self.conv0_3 = ConvBlock(filters[0]*3 + filters[1], filters[0])\n        self.conv0_4 = ConvBlock(filters[0]*4 + filters[1], filters[0])\n        \n        self.conv1_1 = ConvBlock(filters[1] + filters[2], filters[1])\n        self.conv1_2 = ConvBlock(filters[1]*2 + filters[2], filters[1])\n        self.conv1_3 = ConvBlock(filters[1]*3 + filters[2], filters[1])\n        \n        self.conv2_1 = ConvBlock(filters[2] + filters[3], filters[2])\n        self.conv2_2 = ConvBlock(filters[2]*2 + filters[3], filters[2])\n        \n        self.conv3_1 = ConvBlock(filters[3] + filters[4], filters[3])\n        \n        # Pooling & Upsampling\n        self.pool = nn.MaxPool2d(2)\n        self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)\n        \n        # Output\n        self.final = nn.Conv2d(filters[0], num_classes, 1)\n    \n    def forward(self, x):\n        # Encoder\n        x0_0 = self.conv0_0(x)\n        x1_0 = self.conv1_0(self.pool(x0_0))\n        x2_0 = self.conv2_0(self.pool(x1_0))\n        x3_0 = self.conv3_0(self.pool(x2_0))\n        x4_0 = self.conv4_0(self.pool(x3_0))\n        \n        # Nested connections\n        x0_1 = self.conv0_1(torch.cat([x0_0, self.up(x1_0)], 1))\n        x1_1 = self.conv1_1(torch.cat([x1_0, self.up(x2_0)], 1))\n        x2_1 = self.conv2_1(torch.cat([x2_0, self.up(x3_0)], 1))\n        x3_1 = self.conv3_1(torch.cat([x3_0, self.up(x4_0)], 1))\n        \n        x0_2 = self.conv0_2(torch.cat([x0_0, x0_1, self.up(x1_1)], 1))\n        x1_2 = self.conv1_2(torch.cat([x1_0, x1_1, self.up(x2_1)], 1))\n        x2_2 = self.conv2_2(torch.cat([x2_0, x2_1, self.up(x3_1)], 1))\n        \n        x0_3 = self.conv0_3(torch.cat([x0_0, x0_1, x0_2, self.up(x1_2)], 1))\n        x1_3 = self.conv1_3(torch.cat([x1_0, x1_1, x1_2, self.up(x2_2)], 1))\n        \n        x0_4 = self.conv0_4(torch.cat([x0_0, x0_1, x0_2, x0_3, self.up(x1_3)], 1))\n        \n        return self.final(x0_4)\n\n# ============================================================================\n# PREPROCESSING FUNCTIONS\n# ============================================================================\n\ndef normalize_slice(slice_2d, method=\"minmax\", clip_percentile=(1, 99)):\n    \"\"\"Normalize single 2D slice\"\"\"\n    if clip_percentile:\n        low, high = np.percentile(slice_2d, clip_percentile)\n        slice_2d = np.clip(slice_2d, low, high)\n\n    if method == \"minmax\":\n        img_min = slice_2d.min()\n        img_max = slice_2d.max()\n        if img_max - img_min > 0:\n            normalized = (slice_2d - img_min) / (img_max - img_min)\n        else:\n            normalized = slice_2d - img_min\n    elif method == \"standardize\":\n        mean = slice_2d.mean()\n        std = slice_2d.std()\n        if std > 0:\n            normalized = (slice_2d - mean) / std\n        else:\n            normalized = slice_2d - mean\n    else:\n        normalized = slice_2d\n\n    return normalized.astype(np.float32)\n\ndef normalize_volume(volume, method=\"minmax\", clip_percentile=(1, 99)):\n    \"\"\"Normalize entire 3D volume\"\"\"\n    if clip_percentile:\n        low, high = np.percentile(volume, clip_percentile)\n        volume = np.clip(volume, low, high)\n\n    if method == \"minmax\":\n        vol_min = volume.min()\n        vol_max = volume.max()\n        if vol_max - vol_min > 0:\n            normalized = (volume - vol_min) / (vol_max - vol_min)\n        else:\n            normalized = volume - vol_min\n    elif method == \"standardize\":\n        mean = volume.mean()\n        std = volume.std()\n        if std > 0:\n            normalized = (volume - mean) / std\n        else:\n            normalized = volume - mean\n    else:\n        normalized = volume\n\n    return normalized.astype(np.float32)\n\n# ============================================================================\n# THRESHOLDING METHODS\n# ============================================================================\n\ndef apply_otsu_threshold(probability_map):\n    \"\"\"Apply Otsu's method for automatic thresholding\"\"\"\n    prob_uint8 = (probability_map * 255).astype(np.uint8)\n    \n    hist, bin_edges = np.histogram(prob_uint8.ravel(), bins=256, range=(0, 256))\n    bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2\n    \n    hist = hist.astype(float)\n    hist /= hist.sum()\n    \n    weight1 = np.cumsum(hist)\n    weight2 = np.cumsum(hist[::-1])[::-1]\n    \n    mean1 = np.cumsum(hist * bin_centers) / (weight1 + 1e-10)\n    mean2 = (np.cumsum((hist * bin_centers)[::-1]) / (weight2 + 1e-10))[::-1]\n    \n    variance = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:]) ** 2\n    \n    idx = np.argmax(variance)\n    threshold = bin_centers[idx] / 255.0\n    \n    return threshold\n\ndef apply_adaptive_threshold(probability_map, base_threshold=0.5):\n    \"\"\"Adaptive thresholding based on local statistics\"\"\"\n    mean_prob = probability_map.mean()\n    std_prob = probability_map.std()\n    \n    if mean_prob < 0.1:\n        threshold = base_threshold - 0.1\n    elif mean_prob > 0.5:\n        threshold = base_threshold + 0.15\n    else:\n        threshold = base_threshold + (mean_prob - 0.3) * 0.3\n    \n    threshold = np.clip(threshold, 0.4, 0.8)\n    \n    return threshold\n\n# ============================================================================\n# POST-PROCESSING FUNCTIONS\n# ============================================================================\n\ndef apply_morphological_operations(binary_mask, config):\n    \"\"\"Apply morphological operations to clean up mask\"\"\"\n    processed = binary_mask.copy()\n    \n    if config.MORPHOLOGICAL_OPENING:\n        processed = binary_opening(processed, iterations=config.OPENING_ITERATIONS)\n    \n    if config.MORPHOLOGICAL_CLOSING:\n        processed = binary_closing(processed, iterations=config.CLOSING_ITERATIONS)\n    \n    if config.EROSION_ITERATIONS > 0:\n        processed = binary_erosion(processed, iterations=config.EROSION_ITERATIONS)\n    \n    return processed\n\ndef filter_components_by_size(binary_mask, min_size=50, max_size=None):\n    \"\"\"Remove components that are too small or too large\"\"\"\n    labeled_mask, num_features = connected_components(binary_mask)\n    \n    filtered_mask = np.zeros_like(binary_mask)\n    \n    for label_id in range(1, num_features + 1):\n        component = (labeled_mask == label_id)\n        component_size = component.sum()\n        \n        if min_size and component_size < min_size:\n            continue\n        if max_size and component_size > max_size:\n            continue\n        \n        filtered_mask[component] = 1\n    \n    return filtered_mask\n\ndef filter_components_by_confidence(binary_mask, probability_map, min_confidence=0.6):\n    \"\"\"Remove components with low average confidence\"\"\"\n    labeled_mask, num_features = connected_components(binary_mask)\n    \n    filtered_mask = np.zeros_like(binary_mask)\n    \n    for label_id in range(1, num_features + 1):\n        component = (labeled_mask == label_id)\n        avg_confidence = probability_map[component].mean()\n        \n        if avg_confidence >= min_confidence:\n            filtered_mask[component] = 1\n    \n    return filtered_mask\n\ndef post_process_slice(binary_mask, probability_map, config):\n    \"\"\"Complete post-processing pipeline for a single slice\"\"\"\n    if not config.USE_POST_PROCESSING:\n        return binary_mask\n    \n    processed = binary_mask.copy()\n    \n    processed = apply_morphological_operations(processed, config)\n    \n    if config.MIN_COMPONENT_SIZE or config.MAX_COMPONENT_SIZE:\n        processed = filter_components_by_size(\n            processed, \n            config.MIN_COMPONENT_SIZE, \n            config.MAX_COMPONENT_SIZE\n        )\n    \n    if config.USE_CONFIDENCE_FILTERING:\n        processed = filter_components_by_confidence(\n            processed, \n            probability_map, \n            config.MIN_CONFIDENCE\n        )\n    \n    return processed\n\n# ============================================================================\n# TEST-TIME AUGMENTATION (TTA)\n# ============================================================================\n\ndef apply_tta_transform(patch, transform_type):\n    \"\"\"Apply TTA transformation\"\"\"\n    if transform_type == 'none':\n        return patch\n    elif transform_type == 'horizontal':\n        return np.flip(patch, axis=1)\n    elif transform_type == 'vertical':\n        return np.flip(patch, axis=0)\n    else:\n        return patch\n\ndef reverse_tta_transform(prediction, transform_type):\n    \"\"\"Reverse TTA transformation\"\"\"\n    if transform_type == 'none':\n        return prediction\n    elif transform_type == 'horizontal':\n        return np.flip(prediction, axis=1)\n    elif transform_type == 'vertical':\n        return np.flip(prediction, axis=0)\n    else:\n        return prediction\n\n# ============================================================================\n# PREDICTION FUNCTIONS\n# ============================================================================\n\ndef extract_patches_for_prediction(slice_2d, patch_size=256, stride=256):\n    \"\"\"Extract patches from slice for prediction\"\"\"\n    h, w = slice_2d.shape\n    patches = []\n    positions = []\n    \n    y_positions = list(range(0, h - patch_size + 1, stride))\n    x_positions = list(range(0, w - patch_size + 1, stride))\n    \n    if y_positions[-1] + patch_size < h:\n        y_positions.append(h - patch_size)\n    if x_positions[-1] + patch_size < w:\n        x_positions.append(w - patch_size)\n    \n    for y in y_positions:\n        for x in x_positions:\n            patch = slice_2d[y:y+patch_size, x:x+patch_size]\n            if patch.shape == (patch_size, patch_size):\n                patches.append(patch)\n                positions.append((y, x))\n    \n    return patches, positions\n\ndef reconstruct_slice_from_patches(predictions, positions, slice_shape, patch_size=256):\n    \"\"\"Reconstruct full slice from patch predictions\"\"\"\n    h, w = slice_shape\n    \n    mask_sum = np.zeros((h, w), dtype=np.float32)\n    count = np.zeros((h, w), dtype=np.int32)\n    \n    for pred, (y, x) in zip(predictions, positions):\n        mask_sum[y:y+patch_size, x:x+patch_size] += pred\n        count[y:y+patch_size, x:x+patch_size] += 1\n    \n    mask = np.zeros_like(mask_sum)\n    mask[count > 0] = mask_sum[count > 0] / count[count > 0]\n    \n    return mask\n\n@torch.no_grad()\ndef predict_volume(model, volume, config, verbose=False):\n    \"\"\"Predict mask for entire 3D volume\"\"\"\n    model.eval()\n    \n    depth, height, width = volume.shape\n    predicted_volume = np.zeros((depth, height, width), dtype=np.uint8)\n    \n    if verbose:\n        print(f\"   Shape: {volume.shape}\")\n        print(f\"   Threshold strategy: {config.THRESHOLD_STRATEGY}\")\n        print(f\"   Post-processing: {'ON' if config.USE_POST_PROCESSING else 'OFF'}\")\n    \n    if config.NORMALIZE_PER_VOLUME:\n        volume = normalize_volume(volume, config.NORM_METHOD, config.CLIP_PERCENTILE)\n    \n    slice_stats = []\n    \n    iterator = tqdm(range(depth), desc=\"   Processing\", leave=False, disable=not verbose)\n    for slice_idx in iterator:\n        slice_2d = volume[slice_idx]\n        \n        if not config.NORMALIZE_PER_VOLUME:\n            slice_2d = normalize_slice(slice_2d, config.NORM_METHOD, config.CLIP_PERCENTILE)\n        \n        patches, positions = extract_patches_for_prediction(\n            slice_2d, config.PATCH_SIZE, config.STRIDE\n        )\n        \n        if len(patches) == 0:\n            continue\n        \n        all_predictions = []\n        \n        if config.USE_TTA:\n            for transform in config.TTA_FLIPS:\n                predictions = []\n                for i in range(0, len(patches), config.BATCH_SIZE):\n                    batch_patches = patches[i:i + config.BATCH_SIZE]\n                    \n                    batch_patches_aug = [apply_tta_transform(p, transform) for p in batch_patches]\n                    \n                    batch_tensor = torch.stack([\n                        torch.from_numpy(p).unsqueeze(0) for p in batch_patches_aug\n                    ]).to(config.DEVICE)\n                    \n                    with torch.cuda.amp.autocast():\n                        outputs = model(batch_tensor)\n                        preds = torch.sigmoid(outputs).cpu().numpy()\n                    \n                    preds_reversed = [reverse_tta_transform(p[0], transform) for p in preds]\n                    predictions.extend(preds_reversed)\n                \n                all_predictions.append(predictions)\n            \n            predictions = [np.mean([all_predictions[j][i] for j in range(len(all_predictions))], axis=0) \n                          for i in range(len(patches))]\n        else:\n            predictions = []\n            for i in range(0, len(patches), config.BATCH_SIZE):\n                batch_patches = patches[i:i + config.BATCH_SIZE]\n                \n                batch_tensor = torch.stack([\n                    torch.from_numpy(p).unsqueeze(0) for p in batch_patches\n                ]).to(config.DEVICE)\n                \n                with torch.cuda.amp.autocast():\n                    outputs = model(batch_tensor)\n                    preds = torch.sigmoid(outputs).cpu().numpy()\n                \n                predictions.extend([p[0] for p in preds])\n        \n        probability_map = reconstruct_slice_from_patches(\n            predictions, positions, (height, width), config.PATCH_SIZE\n        )\n        \n        if config.THRESHOLD_STRATEGY == \"fixed\":\n            threshold = config.FIXED_THRESHOLD\n        elif config.THRESHOLD_STRATEGY == \"adaptive\":\n            threshold = apply_adaptive_threshold(probability_map, config.FIXED_THRESHOLD)\n        elif config.THRESHOLD_STRATEGY == \"otsu\":\n            threshold = apply_otsu_threshold(probability_map)\n        else:\n            threshold = config.FIXED_THRESHOLD\n        \n        mask_binary = (probability_map > threshold).astype(np.uint8)\n        \n        mask_processed = post_process_slice(mask_binary, probability_map, config)\n        \n        predicted_volume[slice_idx] = mask_processed\n        \n        slice_stats.append({\n            'threshold': threshold,\n            'positive_ratio': mask_processed.sum() / mask_processed.size,\n            'num_components': connected_components(mask_processed)[1]\n        })\n    \n    if verbose and len(slice_stats) > 0:\n        avg_threshold = np.mean([s['threshold'] for s in slice_stats])\n        avg_positive = np.mean([s['positive_ratio'] for s in slice_stats]) * 100\n        print(f\"   Avg threshold: {avg_threshold:.3f}\")\n        print(f\"   Avg positive ratio: {avg_positive:.2f}%\")\n    \n    return predicted_volume\n\n# ============================================================================\n# MAIN PREDICTION PIPELINE\n# ============================================================================\n\ndef create_submission(verbose=True):\n    \"\"\"Main function to create submission\"\"\"\n    config = PredictionConfig()\n    \n    Path(config.OUTPUT_DIR).mkdir(parents=True, exist_ok=True)\n    \n    if verbose:\n        print(\"\\n\" + \"=\"*70)\n        print(\"🚀 PREDICTION PIPELINE\")\n        print(\"=\"*70)\n        print(f\"📊 Configuration:\")\n        print(f\"   • Threshold strategy: {config.THRESHOLD_STRATEGY}\")\n        print(f\"   • Fixed threshold: {config.FIXED_THRESHOLD}\")\n        print(f\"   • Post-processing: {config.USE_POST_PROCESSING}\")\n        print(f\"   • Min component size: {config.MIN_COMPONENT_SIZE}\")\n        print(f\"   • TTA: {config.USE_TTA}\")\n        print(\"=\"*70 + \"\\n\")\n    \n    # Load model\n    model = LightweightUNetPlusPlus(\n        in_channels=config.IN_CHANNELS,\n        num_classes=config.NUM_CLASSES,\n        base_filters=config.BASE_FILTERS\n    ).to(config.DEVICE)\n    \n    checkpoint = torch.load(config.MODEL_PATH, map_location=config.DEVICE)\n    \n    if isinstance(checkpoint, dict):\n        if 'model_state_dict' in checkpoint:\n            model.load_state_dict(checkpoint['model_state_dict'])\n        else:\n            model.load_state_dict(checkpoint)\n    else:\n        model.load_state_dict(checkpoint)\n    \n    model.eval()\n    \n    # Load test.csv\n    test_csv = pd.read_csv(f\"{config.DATA_DIR}/test.csv\")\n    test_images_dir = Path(config.DATA_DIR) / \"test_images\"\n    \n    predicted_masks = []\n    tiff_reader = SimpleTiffReader()\n    \n    for idx, row in test_csv.iterrows():\n        image_id = str(row['id'])\n        image_path = test_images_dir / f\"{image_id}.tif\"\n        \n        if verbose:\n            print(f\"[{idx+1}/{len(test_csv)}] Processing: {image_id}\")\n        \n        if not image_path.exists():\n            if verbose:\n                print(f\"  ❌ File not found: {image_path}\")\n            continue\n        \n        try:\n            test_volume = tiff_reader.read_multipage_tiff(str(image_path))\n            \n            if verbose:\n                print(f\"  📦 Loaded volume: {test_volume.shape}\")\n            \n            predicted_mask = predict_volume(model, test_volume, config, verbose=verbose)\n            \n            output_path = Path(config.OUTPUT_DIR) / f\"{image_id}.tif\"\n            tiff_reader.write_multipage_tiff(str(output_path), predicted_mask, compress=True)\n            \n            predicted_masks.append(output_path)\n            \n            positive_ratio = predicted_mask.sum() / predicted_mask.size * 100\n            slices_detected = np.sum(np.any(predicted_mask > 0, axis=(1, 2)))\n            \n            if verbose:\n                print(f\"  ✅ Positive ratio: {positive_ratio:.2f}%\")\n                print(f\"  ✅ Slices with detection: {slices_detected}/{predicted_mask.shape[0]}\")\n            \n            if torch.cuda.is_available():\n                torch.cuda.empty_cache()\n                \n        except Exception as e:\n            if verbose:\n                print(f\"  ❌ Error processing {image_id}: {e}\")\n            import traceback\n            traceback.print_exc()\n            continue\n    \n    # Create submission ZIP\n    if len(predicted_masks) > 0:\n        submission_path = Path(config.SUBMISSION_DIR) / \"submission.zip\"\n        \n        with zipfile.ZipFile(submission_path, 'w', zipfile.ZIP_DEFLATED) as zipf:\n            for mask_path in predicted_masks:\n                zipf.write(mask_path, mask_path.name)\n        \n        if verbose:\n            print(\"\\n\" + \"=\"*70)\n            print(f\"✅ SUBMISSION CREATED: {submission_path}\")\n            print(f\"📦 Total files: {len(predicted_masks)}\")\n            print(f\"💾 File size: {submission_path.stat().st_size / (1024*1024):.2f} MB\")\n            print(\"=\"*70 + \"\\n\")\n        \n        return submission_path\n    else:\n        if verbose:\n            print(\"\\n❌ Failed to create submission - no valid predictions\")\n        return None\n\n# ============================================================================\n# USAGE\n# ============================================================================\n\nif __name__ == \"__main__\":\n    submission_path = create_submission(verbose=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-07T16:55:35.180377Z","iopub.execute_input":"2025-12-07T16:55:35.180645Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}