{"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":[{"sourceId":117682,"databundleVersionId":14443416,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# inference_with_progress.py\n# Copy-paste ke sel notebook-mu dan jalankan.\n# Sesuaikan path dan nama model di bagian \"USER CONFIG\"\n\nimport os\nimport sys\nfrom pathlib import Path\nimport numpy as np\nimport cv2\nfrom tqdm import tqdm\nimport tensorflow as tf\nfrom skimage import morphology, measure, exposure\nimport math\n\n# -----------------------\n# USER CONFIG (ganti)\n# -----------------------\nIMAGES_FOLDER = \"images\"        # folder yang berisi slice: 0.png,1.png,...\nDEPTH = 65                      # jumlah slice / depth yang dipakai (ubah sesuai)\nMODEL_PATHS = [\"model.h5\"]      # list: untuk ensemble, masukkan lebih dari 1 model path\nPATCH = 256\nSTRIDE = 128\nSAVE_EVERY = 50                 # simpan progress setiap N patch\nOUTPUT_DIR = \"progress_images\"\nFINAL_MASK_OUT = \"final_mask.png\"\nTHRESHOLD = 0.5\nMIN_SIZE = 250                  # hapus objek kecil (pixel)\n# -----------------------\n\nos.makedirs(OUTPUT_DIR, exist_ok=True)\n\n# -----------------------\n# Utilities: load volume\n# -----------------------\ndef load_volume_from_folder(folder: str, depth=65, ext=\"png\"):\n    folder = Path(folder)\n    vol_slices = []\n    # prefer numeric filenames 0.png, 1.png, ...\n    for i in range(depth):\n        p = folder / f\"{i}.{ext}\"\n        if not p.exists():\n            raise FileNotFoundError(f\"Slice not found: {p}\")\n        img = cv2.imread(str(p), cv2.IMREAD_GRAYSCALE)\n        if img is None:\n            raise ValueError(f\"Cannot read: {p}\")\n        vol_slices.append(img.astype(np.float32) / 255.0)\n    vol = np.stack(vol_slices, axis=-1)  # H, W, D\n    print(\"Loaded volume:\", vol.shape)\n    return vol\n\n# -----------------------\n# CLAHE preprocessing (ditemukan di final.ipynb)\n# -----------------------\ndef clahe_preprocessing(vol_array, clip_limit=0.02, tile_size=8):\n    \"\"\"\n    Apply CLAHE per-slice. vol_array shape: (D, H, W) or (H, W, D).\n    This function handles both orientations.\n    Returns same shape as input.\n    \"\"\"\n    arr = np.asarray(vol_array)\n    # normalize orientation to (D, H, W)\n    flip_back = False\n    if arr.ndim == 3 and arr.shape[2] < arr.shape[0]:  # heuristic\n        # assume input is H,W,D\n        arr = np.transpose(arr, (2,0,1))\n        flip_back = True\n\n    D, H, W = arr.shape\n    enhanced = np.zeros_like(arr, dtype=np.float32)\n    # CLAHE using skimage.exposure.equalize_adapthist\n    for z in range(D):\n        sl = arr[z]\n        # stretch slightly first to avoid degenerate slices\n        if sl.max() - sl.min() > 1e-8:\n            sln = (sl - sl.min()) / (sl.max() - sl.min())\n        else:\n            sln = sl.copy()\n        # clip_limit passed to skimage is different scaling; use default kernel_size\n        cl = exposure.equalize_adapthist(sln, clip_limit=clip_limit, nbins=256)\n        enhanced[z] = cl\n    # restore orientation\n    if flip_back:\n        enhanced = np.transpose(enhanced, (1,2,0))\n    return enhanced\n\n# -----------------------\n# TTA helpers (rot + flips) — enhanced TTA (6 augs)\n# -----------------------\ndef tta_augmentations(img):\n    \"\"\"\n    img: numpy array HxW (single-depth aggregated) or HxW (per-patch)\n    returns list of (aug_img, reverse_fn)\n    \"\"\"\n    outs = []\n    # identity\n    outs.append((img.copy(), lambda x: x))\n    # vflip\n    outs.append((np.flipud(img), lambda x: np.flipud(x)))\n    # hflip\n    outs.append((np.fliplr(img), lambda x: np.fliplr(x)))\n    # rotate90\n    rot90 = np.rot90(img, 1)\n    outs.append((rot90, lambda x: np.rot90(x, -1)))\n    # rotate270\n    rot270 = np.rot90(img, 3)\n    outs.append((rot270, lambda x: np.rot90(x, -3)))\n    # rotate90 + flip\n    outs.append((np.fliplr(rot90), lambda x: np.rot90(np.fliplr(x), -1)))\n    return outs\n\ndef tta_predict_single_model(model, input_patch):\n    \"\"\"\n    input_patch: shape (H, W, D) or (H, W) for single-slice models.\n    This function handles 2D/3D input adapting to model input shape.\n    Returns averaged prediction (H, W)\n    \"\"\"\n    # Determine model expected input dims\n    # We'll assume model expects (batch, H, W, C) or (batch, H, W, D)\n    # We'll run TTA on 2D projection if input is 3D (average across depth)\n    x_in = input_patch\n    if x_in.ndim == 3:\n        # simple approach: take central slice or mean across depth\n        # use mean projection for robustness\n        proj = np.mean(x_in, axis=2)\n    else:\n        proj = x_in\n\n    augs = tta_augmentations(proj)\n    preds = []\n    for aug_img, reverse_fn in augs:\n        # prepare model input shape:\n        m_in = aug_img.astype(np.float32)\n        # expand channels if model requires channel dim\n        if m_in.ndim == 2:\n            m_in = m_in[..., np.newaxis]\n        m_in = np.expand_dims(m_in, 0)  # add batch\n        # model.predict expects same size — if not, user must ensure patch sizes match\n        p = model.predict(m_in)[0]\n        # if model outputs (H, W, 1) or (H, W)\n        if p.ndim == 3 and p.shape[2] == 1:\n            p2 = p[...,0]\n        elif p.ndim == 2:\n            p2 = p\n        else:\n            # if model outputs channels >1, assume first channel is mask\n            p2 = p[...,0]\n        # reverse augmentation to align with original orientation\n        p2r = reverse_fn(p2)\n        preds.append(p2r)\n    preds = np.stack(preds, axis=0)\n    avg = preds.mean(axis=0)\n    return avg\n\n# -----------------------\n# Ensemble helper\n# -----------------------\ndef ensemble_tta_predict(models, patch):\n    \"\"\"\n    models: list of keras models\n    patch: H x W x D or H x W\n    Returns averaged prediction across models and TTA.\n    \"\"\"\n    model_preds = []\n    for m in models:\n        p = tta_predict_single_model(m, patch)\n        model_preds.append(p)\n    return np.stack(model_preds, axis=0).mean(axis=0)\n\n# -----------------------\n# Postprocessing helper\n# -----------------------\ndef refine_prediction_output(pred_mask, threshold=0.5, minimum_object_size=MIN_SIZE):\n    \"\"\"\n    threshold -> binary mask, remove small objects\n    \"\"\"\n    binm = (pred_mask >= threshold).astype(np.uint8)\n    # remove small objects\n    cleaned = morphology.remove_small_objects(binm.astype(bool), min_size=minimum_object_size)\n    cleaned = cleaned.astype(np.uint8)\n    return cleaned\n\n# -----------------------\n# Save progress image\n# -----------------------\ndef save_progress_image(canvas, count_map, step, outdir=OUTPUT_DIR):\n    combined = canvas / (count_map + 1e-8)\n    img = np.nan_to_num(combined)\n    # normalize 0..255\n    if img.max() - img.min() > 1e-8:\n        imgn = (img - img.min()) / (img.max() - img.min())\n    else:\n        imgn = img\n    img8 = (imgn * 255).astype(np.uint8)\n    fname = os.path.join(outdir, f\"progress_{step:05d}.png\")\n    cv2.imwrite(fname, img8)\n    print(\"[saved]\", fname)\n\n# -----------------------\n# Sliding window helper\n# -----------------------\ndef make_patch_coords(H, W, patch=PATCH, stride=STRIDE):\n    coords = []\n    for y in range(0, H - patch + 1, stride):\n        for x in range(0, W - patch + 1, stride):\n            coords.append((x,y))\n    # ensure coverage of right/bottom edges (pad last if necessary)\n    if coords:\n        maxx = max(c[0] for c in coords)\n        maxy = max(c[1] for c in coords)\n        if maxx + patch < W:\n            # add column at right\n            for y in sorted(set([c[1] for c in coords])):\n                coords.append((W - patch, y))\n        if maxy + patch < H:\n            # add row at bottom\n            for x in sorted(set([c[0] for c in coords])):\n                coords.append((x, H - patch))\n        # corner\n        if (W - patch, H - patch) not in coords:\n            coords.append((W - patch, H - patch))\n    else:\n        coords.append((0,0))\n    return coords\n\n# -----------------------\n# Main: load models, volume, run inference with progress snapshots\n# -----------------------\ndef main():\n    # Load volume\n    vol = load_volume_from_folder(IMAGES_FOLDER, depth=DEPTH, ext=\"png\")  # H x W x D\n    H, W, D = vol.shape\n    print(\"Volume shape:\", H, W, D)\n\n    # Apply CLAHE (per-slice) if desired\n    print(\"Applying CLAHE per-slice...\")\n    vol_clahe = clahe_preprocessing(np.transpose(vol, (2,0,1)), clip_limit=0.02)  # input as D,H,W\n    # restore to H,W,D\n    vol_clahe = np.transpose(vol_clahe, (1,2,0))\n    # Use vol_clahe from now on\n    vol = vol_clahe\n\n    # Load models (ensemble support)\n    models = []\n    for p in MODEL_PATHS:\n        print(\"Loading model:\", p)\n        models.append(tf.keras.models.load_model(p, compile=False))\n    print(f\"{len(models)} model(s) loaded.\")\n\n    # Prepare canvas & count_map\n    canvas = np.zeros((H, W), dtype=np.float32)\n    count_map = np.zeros((H, W), dtype=np.float32)\n\n    coords = make_patch_coords(H, W, patch=PATCH, stride=STRIDE)\n    print(\"Total patches:\", len(coords))\n\n    step = 0\n    for (x, y) in tqdm(coords, desc=\"Patches\"):\n        # extract patch (Hpatch x Wpatch x D)\n        ph = vol[y:y+PATCH, x:x+PATCH, :]\n        # if shape smaller than PATCH because near edge - pad\n        if ph.shape[0] != PATCH or ph.shape[1] != PATCH:\n            ph_p = np.zeros((PATCH, PATCH, D), dtype=ph.dtype)\n            ph_p[:ph.shape[0], :ph.shape[1], :] = ph\n            ph = ph_p\n\n        # ensemble + TTA predict -> get H x W predicted probabilities\n        pred = ensemble_tta_predict(models, ph)  # returns PATCH x PATCH\n\n        # accumulate (note: if ph was padded, only add real region)\n        h_eff = min(PATCH, H - y)\n        w_eff = min(PATCH, W - x)\n        canvas[y:y+h_eff, x:x+w_eff] += pred[:h_eff, :w_eff]\n        count_map[y:y+h_eff, x:x+w_eff] += 1.0\n\n        step += 1\n        if step % SAVE_EVERY == 0:\n            save_progress_image(canvas, count_map, step)\n\n    # final save\n    save_progress_image(canvas, count_map, step + 1)\n\n    # compute averaged probability map\n    prob_map = canvas / (count_map + 1e-8)\n\n    # post-process -> binary mask\n    mask = refine_prediction_output(prob_map, threshold=THRESHOLD, minimum_object_size=MIN_SIZE)\n\n    # save final mask\n    # normalize mask for saving\n    cv2.imwrite(FINAL_MASK_OUT, (mask*255).astype(np.uint8))\n    print(\"Final mask saved to:\", FINAL_MASK_OUT)\n\n    # Optionally show quick preview using matplotlib (if running in notebook)\n    try:\n        import matplotlib.pyplot as plt\n        plt.figure(figsize=(10,10))\n        plt.imshow(prob_map, cmap='gray')\n        plt.title(\"Final probability map\")\n        plt.axis('off')\n        plt.show()\n    except Exception:\n        pass\n\nif __name__ == \"__main__\":\n    main()\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-08T01:36:43.886806Z","iopub.execute_input":"2025-12-08T01:36:43.887062Z","iopub.status.idle":"2025-12-08T01:37:06.94329Z","shell.execute_reply.started":"2025-12-08T01:36:43.887039Z","shell.execute_reply":"2025-12-08T01:37:06.941871Z"}},"outputs":[{"name":"stderr","text":"2025-12-08 01:36:45.999862: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:477] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\nWARNING: All log messages before absl::InitializeLog() is called are written to STDERR\nE0000 00:00:1765157806.240384      47 cuda_dnn.cc:8310] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\nE0000 00:00:1765157806.312691      47 cuda_blas.cc:1418] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n","output_type":"stream"},{"traceback":["\u001b[0;31m---------------------------------------------------------------------------\u001b[0m","\u001b[0;31mAttributeError\u001b[0m                            Traceback (most recent call last)","\u001b[0;31mAttributeError\u001b[0m: 'MessageFactory' object has no attribute 'GetPrototype'"],"ename":"AttributeError","evalue":"'MessageFactory' object has no attribute 'GetPrototype'","output_type":"error"},{"traceback":["\u001b[0;31m---------------------------------------------------------------------------\u001b[0m","\u001b[0;31mAttributeError\u001b[0m                            Traceback (most recent call last)","\u001b[0;31mAttributeError\u001b[0m: 'MessageFactory' object has no attribute 'GetPrototype'"],"ename":"AttributeError","evalue":"'MessageFactory' object has no attribute 'GetPrototype'","output_type":"error"},{"traceback":["\u001b[0;31m---------------------------------------------------------------------------\u001b[0m","\u001b[0;31mAttributeError\u001b[0m                            Traceback (most recent call last)","\u001b[0;31mAttributeError\u001b[0m: 'MessageFactory' object has no attribute 'GetPrototype'"],"ename":"AttributeError","evalue":"'MessageFactory' object has no attribute 'GetPrototype'","output_type":"error"},{"traceback":["\u001b[0;31m---------------------------------------------------------------------------\u001b[0m","\u001b[0;31mAttributeError\u001b[0m                            Traceback (most recent call last)","\u001b[0;31mAttributeError\u001b[0m: 'MessageFactory' object has no attribute 'GetPrototype'"],"ename":"AttributeError","evalue":"'MessageFactory' object has no attribute 'GetPrototype'","output_type":"error"},{"traceback":["\u001b[0;31m---------------------------------------------------------------------------\u001b[0m","\u001b[0;31mAttributeError\u001b[0m                            Traceback (most recent call last)","\u001b[0;31mAttributeError\u001b[0m: 'MessageFactory' object has no attribute 'GetPrototype'"],"ename":"AttributeError","evalue":"'MessageFactory' object has no attribute 'GetPrototype'","output_type":"error"},{"traceback":["\u001b[0;31m---------------------------------------------------------------------------\u001b[0m","\u001b[0;31mFileNotFoundError\u001b[0m                         Traceback (most recent call last)","\u001b[0;32m/tmp/ipykernel_47/1705642341.py\u001b[0m in \u001b[0;36m<cell line: 0>\u001b[0;34m()\u001b[0m\n\u001b[1;32m    303\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    304\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0m__name__\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"__main__\"\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 305\u001b[0;31m     \u001b[0mmain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m","\u001b[0;32m/tmp/ipykernel_47/1705642341.py\u001b[0m in \u001b[0;36mmain\u001b[0;34m()\u001b[0m\n\u001b[1;32m    228\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mmain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    229\u001b[0m     \u001b[0;31m# Load volume\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 230\u001b[0;31m     \u001b[0mvol\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mload_volume_from_folder\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mIMAGES_FOLDER\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdepth\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mDEPTH\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mext\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m\"png\"\u001b[0m\u001b[0;34m)\u001b[0m  \u001b[0;31m# H x W x D\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    231\u001b[0m     \u001b[0mH\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mW\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mD\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mvol\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    232\u001b[0m     \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Volume shape:\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mH\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mW\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mD\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n","\u001b[0;32m/tmp/ipykernel_47/1705642341.py\u001b[0m in \u001b[0;36mload_volume_from_folder\u001b[0;34m(folder, depth, ext)\u001b[0m\n\u001b[1;32m     40\u001b[0m         \u001b[0mp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfolder\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0;34mf\"{i}.{ext}\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     41\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexists\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 42\u001b[0;31m             \u001b[0;32mraise\u001b[0m \u001b[0mFileNotFoundError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"Slice not found: {p}\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     43\u001b[0m         \u001b[0mimg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcv2\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mimread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mp\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcv2\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mIMREAD_GRAYSCALE\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     44\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mimg\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n","\u001b[0;31mFileNotFoundError\u001b[0m: Slice not found: images/0.png"],"ename":"FileNotFoundError","evalue":"Slice not found: images/0.png","output_type":"error"}],"execution_count":1}]}