{"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":"gpu","dataSources":[{"sourceId":117682,"databundleVersionId":15062069,"sourceType":"competition"},{"sourceId":14245247,"sourceType":"datasetVersion","datasetId":9088503},{"sourceId":14295835,"sourceType":"datasetVersion","datasetId":9125518},{"sourceId":288572598,"sourceType":"kernelVersion"},{"sourceId":294535277,"sourceType":"kernelVersion"}],"dockerImageVersionId":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# %% [code]\n\"\"\"\n================================================================================\n   VESUVIUS V19 - THE REFINED CHAMPION\n\n   - Base: V16 (Best Score: 0.533)\n   - Overlap: 0.25 (Proven best for sharpness)\n   - Thresholds: 0.85 / 0.45 (Proven best balance)\n   - Optimization: LOGIT Averaging (Mathematically superior to Prob averaging)\n================================================================================\n\"\"\"\n\nimport os\nos.environ[\"KERAS_BACKEND\"] = \"tensorflow\"\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"2\"\n\n# ============================================================================\n# SETUP PACKAGES\n# ============================================================================\nvar = \"/kaggle/input/vesuvius25-packages-offline-installer-v20251226/whls\"\nif os.path.exists(var):\n    print(f\"Installing packages from: {var}\")\n    import subprocess\n    subprocess.run([\n        \"pip\", \"install\", \"--quiet\",\n        f\"{var}/keras_nightly-3.12.0.dev2025100703-py3-none-any.whl\",\n        f\"{var}/tifffile-2025.10.16-py3-none-any.whl\",\n        f\"{var}/imagecodecs-2025.11.11-cp311-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl\",\n        f\"{var}/medicai-0.0.3-py3-none-any.whl\",\n        \"--no-index\",\n        \"--find-links\", var\n    ], check=False, capture_output=True)\n\nimport keras\nfrom keras import ops\nfrom medicai.transforms import Compose, ScaleIntensityRange\nfrom medicai.models import TransUNet\nfrom medicai.utils.inference import SlidingWindowInference\n\nimport numpy as np\nimport pandas as pd\nimport zipfile\nimport tifffile\nimport scipy.ndimage as ndi\nfrom skimage.morphology import remove_small_objects\nimport gc\n\nprint(\"=\"*60)\nprint(\"VESUVIUS V19 - LOGIT ENSEMBLE + V16 SETTINGS\")\nprint(\"=\"*60)\n\n# ============================================================================\n# CONFIG\n# ============================================================================\nroot_dir = \"/kaggle/input/vesuvius-challenge-surface-detection\"\ntest_dir = f\"{root_dir}/test_images\"\noutput_dir = \"/kaggle/working/submission_masks\"\nzip_path = \"/kaggle/working/submission.zip\"\nos.makedirs(output_dir, exist_ok=True)\n\n# Model config\nNUM_CLASSES = 3\nPATCH_SIZE = (160, 160, 160)\nOVERLAP = 0.25  # STRICTLY 0.25 (The V16 Winner)\n\n# Post-processing config (V16 Winner Settings)\nT_LOW = 0.45    # Permissive connectivity\nT_HIGH = 0.85   # High confidence seeds\nZ_RADIUS = 1\nXY_RADIUS = 0\nDUST_MIN_SIZE = 500\n\n# ============================================================================\n# MODEL LOADING (LOGIT MODE)\n# ============================================================================\ndef get_ensemble_models():\n    # 1. Baseline Model (Weight: 0.25)\n    path1 = \"/kaggle/input/train-transunet-baseline-lb-0-537/fine_tuning_epoch_20.weights.h5\"\n    # 2. Stronger Model (Weight: 0.75)\n    path2 = \"/kaggle/input/train-vesuvius-surface-3d-detection-on-tpu/model.weights.h5\"\n    \n    models = []\n    \n    # classifier_activation=None -> Output Logits\n    if os.path.exists(path1):\n        print(f\"Loading Model 1: {path1}\")\n        m1 = TransUNet(input_shape=(160, 160, 160, 1), encoder_name='seresnext50', classifier_activation=None, num_classes=NUM_CLASSES)\n        m1.load_weights(path1)\n        models.append(m1)\n    \n    if os.path.exists(path2):\n        print(f\"Loading Model 2: {path2}\")\n        m2 = TransUNet(input_shape=(160, 160, 160, 1), encoder_name='seresnext50', classifier_activation=None, num_classes=NUM_CLASSES)\n        m2.load_weights(path2)\n        models.append(m2)\n        \n    if not models:\n        print(\"WARNING: No weights found. Initializing random model.\")\n        m = TransUNet(input_shape=(160, 160, 160, 1), encoder_name='seresnext50', classifier_activation=None, num_classes=NUM_CLASSES)\n        return [m]\n        \n    return models\n\n# ============================================================================\n# TRANSFORMS & HELPERS\n# ============================================================================\ndef val_transformation(image):\n    data = {\"image\": image}\n    pipeline = Compose([ScaleIntensityRange(keys=[\"image\"], a_min=0, a_max=255, b_min=0, b_max=1, clip=True)])\n    result = pipeline(data)\n    return result[\"image\"]\n\ndef load_volume(path):\n    vol = tifffile.imread(path).astype(np.float32)\n    vol = vol[None, ..., None]\n    return vol\n\n# ============================================================================\n# INFERENCE LOGIC (4x TTA + LOGITS)\n# ============================================================================\ndef predict_with_tta(inputs, swi):\n    \"\"\"4x TTA (Rotations only) - Returns LOGITS.\"\"\"\n    logits = []\n    # Original\n    logits.append(swi(inputs))\n    # Rotations\n    for k in [1, 2, 3]:\n        img_r = np.rot90(inputs, k=k, axes=(2, 3))\n        p = swi(img_r)\n        p = np.rot90(p, k=-k, axes=(2, 3))\n        logits.append(p)\n    return np.mean(logits, axis=0)\n\ndef ensemble_predict(inputs, models):\n    # Weighted Ensemble\n    weights = [0.25, 0.75] if len(models) == 2 else [1.0/len(models)]*len(models)\n    ensemble_logits = []\n    \n    for i, model in enumerate(models):\n        swi = SlidingWindowInference(model, num_classes=NUM_CLASSES, roi_size=PATCH_SIZE, sw_batch_size=1, mode='gaussian', overlap=OVERLAP)\n        \n        # Get Logits\n        logits = predict_with_tta(inputs, swi)\n        ensemble_logits.append(logits * weights[i])\n    \n    # Sum weighted logits\n    total_logits = np.sum(ensemble_logits, axis=0)\n    \n    # Softmax to get Probabilities\n    probs = ops.softmax(total_logits, axis=-1)\n    \n    # Return Foreground Probability (Class 1)\n    return np.squeeze(probs[..., 1])\n\n# ============================================================================\n# POST-PROCESSING (HYSTERESIS)\n# ============================================================================\ndef build_anisotropic_struct(z_radius, xy_radius):\n    z, r = z_radius, xy_radius\n    if z == 0 and r == 0: return None\n    depth = 2 * z + 1\n    size = 2 * r + 1\n    struct = np.zeros((depth, size, size), dtype=bool)\n    cz, cy, cx = z, r, r\n    for dz in range(-z, z + 1):\n        for dy in range(-r, r + 1):\n            for dx in range(-r, r + 1):\n                if dy * dy + dx * dx <= r * r:\n                    struct[cz + dz, cy + dy, cx + dx] = True\n    return struct\n\ndef topo_postprocess(probs, T_low, T_high, z_radius, xy_radius, dust_min_size):\n    # 1. Hysteresis Thresholding\n    strong = probs >= T_high\n    weak = probs >= T_low\n    \n    if not strong.any(): return np.zeros_like(probs, dtype=np.uint8)\n    \n    struct_hyst = ndi.generate_binary_structure(3, 3)\n    mask = ndi.binary_propagation(strong, mask=weak, structure=struct_hyst)\n    \n    if not mask.any(): return np.zeros_like(probs, dtype=np.uint8)\n\n    # 2. Anisotropic Closing\n    struct = build_anisotropic_struct(z_radius, xy_radius)\n    if struct is not None:\n        mask = ndi.binary_closing(mask, structure=struct)\n\n    # 3. Dust Removal\n    if dust_min_size > 0:\n        mask = remove_small_objects(mask.astype(bool), min_size=dust_min_size)\n\n    return mask.astype(np.uint8)\n\n# ============================================================================\n# MAIN PIPELINE\n# ============================================================================\nprint(\"\\nLoading models...\")\nmodels = get_ensemble_models()\ntest_df = pd.read_csv(f\"{root_dir}/test.csv\")\n\nprint(\"\\nStarting Inference...\")\nwith zipfile.ZipFile(zip_path, \"w\", compression=zipfile.ZIP_DEFLATED) as z:\n    for idx, row in test_df.iterrows():\n        image_id = row[\"id\"]\n        print(f\"\\n[{idx+1}/{len(test_df)}] Processing {image_id}...\")\n        \n        # 1. Load\n        vol = load_volume(f\"{test_dir}/{image_id}.tif\")\n        vol = val_transformation(vol)\n        \n        # 2. Predict (Ensemble -> 4x TTA -> Logits -> Softmax)\n        probs = ensemble_predict(vol, models)\n        \n        # 3. Post-process (Hysteresis)\n        final_mask = topo_postprocess(\n            probs, \n            T_low=T_LOW, \n            T_high=T_HIGH, \n            z_radius=Z_RADIUS, \n            xy_radius=XY_RADIUS, \n            dust_min_size=DUST_MIN_SIZE\n        )\n        \n        # 4. Save\n        print(f\"    Foreground voxels: {final_mask.sum():,}\")\n        out_path = f\"{output_dir}/{image_id}.tif\"\n        tifffile.imwrite(out_path, final_mask)\n        z.write(out_path, arcname=f\"{image_id}.tif\")\n        os.remove(out_path)\n        \n        del vol, probs, final_mask\n        gc.collect()\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"V19 COMPLETE\")\nprint(f\"Submission: {zip_path}\")\nprint(\"=\"*60)","metadata":{"_uuid":"0d6e0d6e-c082-4dae-9b70-7a2a04ab4262","_cell_guid":"5ee38ca6-d1f4-4cde-85b6-3effd0b984c4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-31T12:55:09.726903Z","iopub.execute_input":"2026-01-31T12:55:09.727207Z","iopub.status.idle":"2026-01-31T12:58:05.012017Z","shell.execute_reply.started":"2026-01-31T12:55:09.727182Z","shell.execute_reply":"2026-01-31T12:58:05.011217Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}