{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":30201,"databundleVersionId":2750748,"sourceType":"competition"},{"sourceId":11977273,"sourceType":"datasetVersion","datasetId":7510849},{"sourceId":11986209,"sourceType":"datasetVersion","datasetId":7519117},{"sourceId":11987188,"sourceType":"datasetVersion","datasetId":7510607},{"sourceId":12017642,"sourceType":"datasetVersion","datasetId":7534370},{"sourceId":12025555,"sourceType":"datasetVersion","datasetId":7566006},{"sourceId":12025867,"sourceType":"datasetVersion","datasetId":7565955}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install --no-index --find-links /kaggle/input/detectron2-whls --no-deps yacs portalocker pathspec iopath hydra-core black fvcore detectron2","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:14:51.369020Z","iopub.execute_input":"2025-06-01T14:14:51.369194Z","iopub.status.idle":"2025-06-01T14:14:54.861392Z","shell.execute_reply.started":"2025-06-01T14:14:51.369169Z","shell.execute_reply":"2025-06-01T14:14:54.860732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install --no-index --find-links /kaggle/input/cellpose-whl --no-deps cellpose fastremap fill_voids roifile","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:14:54.862892Z","iopub.execute_input":"2025-06-01T14:14:54.863148Z","iopub.status.idle":"2025-06-01T14:14:56.943649Z","shell.execute_reply.started":"2025-06-01T14:14:54.863121Z","shell.execute_reply":"2025-06-01T14:14:56.942960Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======================== Detectron2 ========================\nimport detectron2\nimport torch\nfrom detectron2 import model_zoo\nfrom detectron2.engine import DefaultPredictor\nfrom detectron2.config import get_cfg\nfrom PIL import Image\nimport cv2\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom fastcore.all import *\ndetectron2.__version__\nfrom skimage.color import label2rgb\n\n\n# ======================== Cellpose ========================\nfrom cellpose import models, io\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nimport cv2\n\nfrom tqdm import tqdm\n\n\nfrom collections import defaultdict\nfrom tqdm import tqdm\nimport skimage.io as io\nfrom cellpose import dynamics","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:14:56.944572Z","iopub.execute_input":"2025-06-01T14:14:56.944844Z","iopub.status.idle":"2025-06-01T14:15:26.041246Z","shell.execute_reply.started":"2025-06-01T14:14:56.944820Z","shell.execute_reply":"2025-06-01T14:15:26.040627Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Mask R-CNN Prediction","metadata":{}},{"cell_type":"code","source":"# From https://www.kaggle.com/stainsby/fast-tested-rle\ndef rle_decode(mask_rle, shape=(520, 704)):\n    '''\n    mask_rle: run-length as string formated (start length)\n    shape: (height,width) of array to return \n    Returns numpy array, 1 - mask, 0 - background\n\n    '''\n    s = mask_rle.split()\n    starts, lengths = [np.asarray(x, dtype=int) for x in (s[0:][::2], s[1:][::2])]\n    starts -= 1\n    ends = starts + lengths\n    img = np.zeros(shape[0]*shape[1], dtype=np.uint8)\n    for lo, hi in zip(starts, ends):\n        img[lo:hi] = 1\n    return img.reshape(shape)  # Needed to align to RLE direction\n\ndef rle_encode(img):\n    '''\n    img: numpy array, 1 - foreground, 0 - background\n    Returns run length as string\n    '''\n    pixels = img.flatten()\n    pixels = np.concatenate([[0], pixels, [0]])\n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    runs[1::2] -= runs[::2]\n    return ' '.join(str(x) for x in runs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:15:26.041989Z","iopub.execute_input":"2025-06-01T14:15:26.042406Z","iopub.status.idle":"2025-06-01T14:15:26.048440Z","shell.execute_reply.started":"2025-06-01T14:15:26.042387Z","shell.execute_reply":"2025-06-01T14:15:26.047741Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def compute_iou(mask1, mask2):\n    \"\"\"\n    計算兩個二值 mask 的 IoU（Intersection over Union）。\n    \"\"\"\n    intersection = np.logical_and(mask1, mask2).sum()\n    union = np.logical_or(mask1, mask2).sum()\n    return intersection / union if union > 0 else 0.0","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:15:26.050450Z","iopub.execute_input":"2025-06-01T14:15:26.050690Z","iopub.status.idle":"2025-06-01T14:15:26.073323Z","shell.execute_reply.started":"2025-06-01T14:15:26.050673Z","shell.execute_reply":"2025-06-01T14:15:26.072669Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"Dir_testdata=Path('../input/sartorius-cell-instance-segmentation')\nmaskrcnn_ids, maskrcnn_masks = [], []\ntest_image_names = (Dir_testdata/'test').ls()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:15:26.073927Z","iopub.execute_input":"2025-06-01T14:15:26.074167Z","iopub.status.idle":"2025-06-01T14:15:26.105691Z","shell.execute_reply.started":"2025-06-01T14:15:26.074151Z","shell.execute_reply":"2025-06-01T14:15:26.104993Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======================== Detectron2 ========================\ncfg = get_cfg()\ncfg.merge_from_file(model_zoo.get_config_file(\"COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml\"))\ncfg.INPUT.MASK_FORMAT='bitmask'\ncfg.MODEL.ROI_HEADS.NUM_CLASSES = 3 \n\nTHRESHOLDS = [.15, .35, .55]\nMIN_PIXELS = [75, 150, 75]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:15:26.106488Z","iopub.execute_input":"2025-06-01T14:15:26.106713Z","iopub.status.idle":"2025-06-01T14:15:26.119374Z","shell.execute_reply.started":"2025-06-01T14:15:26.106699Z","shell.execute_reply":"2025-06-01T14:15:26.118776Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellpose Prediction","metadata":{}},{"cell_type":"code","source":"def get_patches(image_shape, patch_size, stride):\n    \"\"\"\n    計算圖像分塊的起始座標和結束座標。\n\n    Args:\n        image_shape (tuple): 原始圖像的 (H, W)。\n        patch_size (tuple): 每個分塊的 (patch_H, patch_W)。\n        stride (tuple): 分塊的步長 (stride_H, stride_W)。\n\n    Returns:\n        list: 包含 (y_start, y_end, x_start, x_end) 的元組列表。\n    \"\"\"\n    H, W = image_shape\n    patch_H, patch_W = patch_size\n    stride_H, stride_W = stride\n    patches = []\n\n    # 計算步長，確保覆蓋所有區域\n    if H <= patch_H:\n        y_steps = [0]\n    else:\n        y_steps = np.arange(0, H - patch_H + stride_H, stride_H)\n        if y_steps[-1] < H - patch_H:\n            y_steps = np.append(y_steps, H - patch_H)\n\n    if W <= patch_W:\n        x_steps = [0]\n    else:\n        x_steps = np.arange(0, W - patch_W + stride_W, stride_W)\n        if x_steps[-1] < W - patch_W:\n            x_steps = np.append(x_steps, W - patch_W)\n\n    for y in y_steps:\n        for x in x_steps:\n            y_start = int(y)\n            y_end = int(min(y + patch_H, H))\n            x_start = int(x)\n            x_end = int(min(x + patch_W, W))\n\n            # 調整起始點以確保完整覆蓋邊緣\n            if y_end - y_start < patch_H and y_end == H:\n                y_start = H - patch_H\n            if x_end - x_start < patch_W and x_end == W:\n                x_start = W - patch_W\n\n            y_start = max(0, y_start) # 確保不為負\n            x_start = max(0, x_start) # 確保不為負\n\n            patches.append((y_start, y_end, x_start, x_end))\n    return patches # 確保總是返回 patches 列表，即使它是空的","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:15:26.120115Z","iopub.execute_input":"2025-06-01T14:15:26.120671Z","iopub.status.idle":"2025-06-01T14:15:26.128872Z","shell.execute_reply.started":"2025-06-01T14:15:26.120647Z","shell.execute_reply":"2025-06-01T14:15:26.128277Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def stitch_masks_and_flows(patches_data, original_shape):\n    \"\"\"\n    將多個分塊的機率圖和流場拼接回原始圖像尺寸。\n    重疊區域使用平均值。\n\n    Args:\n        patches_data (list): 每個元素為 (y_start, y_end, x_start, x_end, prob_map, dp_map)\n        original_shape (tuple): 原始圖像的 (H, W)。\n\n    Returns:\n        tuple: (stitched_prob_map, stitched_dp_map)\n    \"\"\"\n    H_orig, W_orig = original_shape\n    stitched_prob_map_total = np.zeros(original_shape, dtype=np.float32)\n    stitched_dp_map_total = np.zeros((2, H_orig, W_orig), dtype=np.float32) # dP 格式為 (2, H, W)\n    pixel_counts = np.zeros(original_shape, dtype=np.float32) # 使用 float 避免溢出\n\n    for y_s, y_e, x_s, x_e, prob_map, dp_map in patches_data:\n        current_prob_h, current_prob_w = prob_map.shape[:2]\n        current_dp_h, current_dp_w = dp_map.shape[1:]\n\n        stitched_prob_map_total[y_s:y_e, x_s:x_e] += prob_map[:current_prob_h, :current_prob_w]\n        stitched_dp_map_total[:, y_s:y_e, x_s:x_e] += dp_map[:, :current_dp_h, :current_dp_w]\n        pixel_counts[y_s:y_e, x_s:x_e] += 1\n\n    pixel_counts[pixel_counts == 0] = 1\n\n    stitched_prob_map = stitched_prob_map_total / pixel_counts\n    stitched_dp_map = stitched_dp_map_total / pixel_counts\n\n    return stitched_prob_map, stitched_dp_map","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:15:26.129688Z","iopub.execute_input":"2025-06-01T14:15:26.130290Z","iopub.status.idle":"2025-06-01T14:15:26.148388Z","shell.execute_reply.started":"2025-06-01T14:15:26.130272Z","shell.execute_reply":"2025-06-01T14:15:26.147745Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\n\ndef visualize_flow(avg_dp, image_id=\"image\"):\n    \"\"\"\n    視覺化 avg_dp (flow) 結果，包含：\n    - dx（水平分量）\n    - dy（垂直分量）\n    - magnitude（模長）\n    \"\"\"\n    if avg_dp.shape[-1] != 2:\n        raise ValueError(\"avg_dp 須為 [H, W, 2] 形狀\")\n\n    dx = avg_dp[:, :, 0]\n    dy = avg_dp[:, :, 1]\n    magnitude = np.sqrt(dx ** 2 + dy ** 2)\n\n    plt.figure(figsize=(15, 4))\n\n    plt.subplot(1, 3, 1)\n    plt.title(f\"{image_id} - Flow dx\")\n    plt.imshow(dx, cmap=\"seismic\")\n    plt.colorbar()\n\n    plt.subplot(1, 3, 2)\n    plt.title(f\"{image_id} - Flow dy\")\n    plt.imshow(dy, cmap=\"seismic\")\n    plt.colorbar()\n\n    plt.subplot(1, 3, 3)\n    plt.title(f\"{image_id} - Flow Magnitude\")\n    plt.imshow(magnitude, cmap=\"viridis\")\n    plt.colorbar()\n\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:15:26.149276Z","iopub.execute_input":"2025-06-01T14:15:26.149525Z","iopub.status.idle":"2025-06-01T14:15:26.168083Z","shell.execute_reply.started":"2025-06-01T14:15:26.149505Z","shell.execute_reply":"2025-06-01T14:15:26.167526Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def resize_patch_to_height(patch_img, target_h):\n    h, w = patch_img.shape[:2]\n    scale = target_h / h\n    target_w = int(w * scale)\n    return cv2.resize(patch_img, (target_w, target_h), interpolation=cv2.INTER_LINEAR)\n\ndef predict_with_maskrcnn_patching(predictor, img_rgb, img_id, patch_size, stride):\n    \"\"\"\n    使用 Mask R-CNN 進行圖像分塊 (patching) 和多尺度 TTA 推論，並拼接結果。\n\n    Args:\n        predictor: Mask R-CNN 的 predictor 實例。\n        img_rgb (np.ndarray): 輸入的原始 RGB 圖像。\n        img_id (str): 圖像的 ID。\n        patch_size (tuple): 分塊的 (patch_H, patch_W)。\n        stride (tuple): 分塊的步長 (stride_H, stride_W)。\n\n    Returns:\n        tuple: (fused_prob_map, fused_dp_map)\n               - fused_prob_map (np.ndarray): 拼接後的 Mask R-CNN 機率圖 (H, W)。\n               - fused_dp_map (np.ndarray): 拼接後的 Mask R-CNN 流向量圖 (2, H, W)。\n    \"\"\"\n    original_shape = img_rgb.shape[:2]\n    PATCH_H, PATCH_W = patch_size\n    STRIDE_H, STRIDE_W = stride\n    resize_heights = [440, 480, 520, 560, 580, 620]\n\n    patches_to_process = get_patches(original_shape, (PATCH_H, PATCH_W), (STRIDE_H, STRIDE_W))\n    all_fused_patch_data = []\n\n    for y_s, y_e, x_s, x_e in patches_to_process:\n        current_patch_img = img_rgb[y_s:y_e, x_s:x_e]\n\n        if current_patch_img.shape[0] < PATCH_H // 2 or current_patch_img.shape[1] < PATCH_W // 2:\n            continue\n\n        prob_accumulator = []\n        dp_accumulator = []\n\n        for h in resize_heights:\n            resized_patch = resize_patch_to_height(current_patch_img, h)\n\n            # 單尺寸 TTA 預測\n            prob_map, dp_map = tta_predict_probability_and_flows(\n                predictor, resized_patch, img_id, model_type=\"maskrcnn\"\n            )\n\n            # resize 回原 patch 尺寸\n            prob_map = cv2.resize(prob_map, (current_patch_img.shape[1], current_patch_img.shape[0]))\n            dp_map = np.stack([\n                cv2.resize(dp_map[0], (current_patch_img.shape[1], current_patch_img.shape[0])),\n                cv2.resize(dp_map[1], (current_patch_img.shape[1], current_patch_img.shape[0]))\n            ], axis=0)\n\n            prob_accumulator.append(prob_map)\n            dp_accumulator.append(dp_map)\n\n        if len(prob_accumulator) == 0:\n            continue  # 若沒有任何尺寸的預測成功，略過此 patch\n\n        averaged_prob_map = np.mean(prob_accumulator, axis=0)\n        averaged_dp_map = np.mean(dp_accumulator, axis=0)\n\n        all_fused_patch_data.append((y_s, y_e, x_s, x_e, averaged_prob_map, averaged_dp_map))\n\n    if not all_fused_patch_data:\n        return np.zeros(original_shape, dtype=np.float32), \\\n               np.zeros((2, *original_shape), dtype=np.float32)\n\n    fused_prob_map, fused_dp_map = stitch_masks_and_flows(all_fused_patch_data, original_shape)\n\n    return fused_prob_map, fused_dp_map","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:15:26.168842Z","iopub.execute_input":"2025-06-01T14:15:26.169069Z","iopub.status.idle":"2025-06-01T14:15:26.183792Z","shell.execute_reply.started":"2025-06-01T14:15:26.169054Z","shell.execute_reply":"2025-06-01T14:15:26.183151Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def tta_predict_probability_and_flows(model, image, image_id, model_type=\"cellpose\"):\n    import torch\n    import numpy as np\n    import cv2\n    from cellpose import dynamics\n\n    augmentations = [\n        lambda x: x,\n        lambda x: np.flip(x, axis=1),\n        lambda x: np.flip(x, axis=0),\n        lambda x: np.flip(np.flip(x, axis=0), axis=1),\n        lambda x: np.rot90(x, k=1),\n        lambda x: np.rot90(x, k=2),\n        lambda x: np.rot90(x, k=3),\n    ]\n\n    inverse_ops_prob = [\n        lambda x: x,\n        lambda x: np.flip(x, axis=1),\n        lambda x: np.flip(x, axis=0),\n        lambda x: np.flip(np.flip(x, axis=0), axis=1),\n        lambda x: np.rot90(x, k=-1),\n        lambda x: np.rot90(x, k=-2),\n        lambda x: np.rot90(x, k=-3),\n    ]\n\n    inverse_ops_dp = [\n        lambda dp: dp,\n        lambda dp: np.stack([np.flip(dp[0], axis=1), -np.flip(dp[1], axis=1)], axis=0),\n        lambda dp: np.stack([-np.flip(dp[0], axis=0), np.flip(dp[1], axis=0)], axis=0),\n        lambda dp: np.stack([-np.flip(np.flip(dp[0], axis=0), axis=1), -np.flip(np.flip(dp[1], axis=0), axis=1)], axis=0),\n        lambda dp: np.stack([np.rot90(dp[1], k=-1), -np.rot90(dp[0], k=-1)], axis=0),\n        lambda dp: np.stack([-np.rot90(dp[0], k=-2), -np.rot90(dp[1], k=-2)], axis=0),\n        lambda dp: np.stack([-np.rot90(dp[1], k=-3), np.rot90(dp[0], k=-3)], axis=0),\n    ]\n\n    all_prob_maps = []\n    all_dp_maps = []\n    H_orig, W_orig = image.shape[:2]\n\n    for idx, aug_func in enumerate(augmentations):\n        augmented_img = np.ascontiguousarray(aug_func(image))\n\n        if model_type == \"cellpose\":\n            _, eval_flows, _ = model.eval(augmented_img, compute_masks=False, progress=False)\n            pred_dp_map_raw = eval_flows[1].astype(np.float32)\n            pred_prob_map_raw = eval_flows[2].astype(np.float32)\n            weight = 1.0  # Cellpose 沒有 score，預設為 1\n\n        elif model_type == \"maskrcnn\":\n            with torch.no_grad():\n                outputs = model(augmented_img)\n            instances = outputs['instances']\n\n            if len(instances) == 0:\n                continue\n\n            pred_classes_all = instances.pred_classes.cpu().numpy()\n            pred_scores_all = instances.scores.cpu().numpy()\n            pred_masks_all = instances.pred_masks.squeeze(1).cpu().numpy()\n\n            keep_mask = [\n                (cls < len(THRESHOLDS) and score >= THRESHOLDS[cls])\n                for cls, score in zip(pred_classes_all, pred_scores_all)\n            ]\n            keep_mask = np.array(keep_mask)\n            pred_classes = pred_classes_all[keep_mask]\n            pred_scores = pred_scores_all[keep_mask]\n            pred_masks = pred_masks_all[keep_mask]\n\n            if len(pred_classes) == 0:\n                print(f\"[{image_id}] TTA {idx} - All instances filtered.\")\n                continue\n\n            combined_mask = np.zeros(augmented_img.shape[:2], dtype=np.uint16)\n            prob_map = np.zeros_like(combined_mask, dtype=np.float32)\n            used = np.zeros_like(combined_mask, dtype=np.uint8)\n            counter = 1\n\n            for j in np.argsort(pred_scores)[::-1]:\n                mlogits = pred_masks[j]\n                class_id = pred_classes[j]\n\n                if np.max(mlogits) <= 1.0:\n                    mprob = mlogits\n                else:\n                    mprob = torch.sigmoid(torch.from_numpy(mlogits)).numpy()\n\n                mbinary = (mprob > 0.5).astype(np.uint8)\n                inst_mask = mbinary * (1 - used)\n\n                min_pixels = MIN_PIXELS[class_id] if class_id < len(MIN_PIXELS) else 20\n                if inst_mask.sum() < min_pixels:\n                    continue\n\n                combined_mask[inst_mask == 1] = counter\n                used += inst_mask\n                prob_map = np.maximum(prob_map, mprob)\n                counter += 1\n\n            if combined_mask.max() == 0:\n                print(f\"[{image_id}] TTA {idx} - No valid masks after filtering.\")\n                continue\n\n            device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n            dp = dynamics.masks_to_flows_gpu(combined_mask.astype(int), device=device)\n            pred_prob_map_raw = prob_map\n            pred_dp_map_raw = dp\n\n            # ✅ 計算該分支的加權值：使用 pred_scores 平均\n            weight = float(np.mean(pred_scores)) if len(pred_scores) > 0 else 1.0\n            weight = np.clip(weight, 0.3, 2.0)\n\n        else:\n            raise ValueError(\"model_type must be 'cellpose' or 'maskrcnn'\")\n\n        pred_prob_map = inverse_ops_prob[idx](pred_prob_map_raw)\n        pred_dp_map = inverse_ops_dp[idx](pred_dp_map_raw)\n\n        if pred_prob_map.shape != (H_orig, W_orig):\n            pred_prob_map = cv2.resize(pred_prob_map, (W_orig, H_orig), interpolation=cv2.INTER_LINEAR)\n        if pred_dp_map.shape[1:] != (H_orig, W_orig):\n            pred_dp_map = np.stack([\n                cv2.resize(pred_dp_map[0], (W_orig, H_orig), interpolation=cv2.INTER_LINEAR),\n                cv2.resize(pred_dp_map[1], (W_orig, H_orig), interpolation=cv2.INTER_LINEAR)\n            ], axis=0)\n\n        all_prob_maps.append((pred_prob_map, weight))\n        all_dp_maps.append((pred_dp_map, weight))\n\n    if len(all_prob_maps) == 0:\n        print(f\"[{image_id}] ❌ 沒有任何 TTA 分支產生有效 mask\")\n        return np.zeros((H_orig, W_orig), dtype=np.float32), np.zeros((H_orig, W_orig, 2), dtype=np.float32)\n\n    # ✅ 加權平均\n    total_weight = sum(w for _, w in all_prob_maps)\n    avg_prob = sum(prob * w for prob, w in all_prob_maps) / (total_weight + 1e-6)\n\n    total_weight_dp = sum(w for _, w in all_dp_maps)\n    avg_dp = sum(dp * w for dp, w in all_dp_maps) / (total_weight_dp + 1e-6)\n\n    return avg_prob, avg_dp","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:15:26.184436Z","iopub.execute_input":"2025-06-01T14:15:26.184704Z","iopub.status.idle":"2025-06-01T14:15:26.207760Z","shell.execute_reply.started":"2025-06-01T14:15:26.184685Z","shell.execute_reply":"2025-06-01T14:15:26.206836Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------- 主推論邏輯開始 -----------------\nfrom pathlib import Path\nfrom tqdm import tqdm\nfrom skimage import io\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom cellpose import dynamics, models\n\n# 測試資料目錄\ntest_dir = Path('/kaggle/input/sartorius-cell-instance-segmentation/test')\ntest_files = sorted([f for f in test_dir.iterdir() if f.suffix == '.png'])\n\nsubmission_data = []\n\n# Patch 參數\nPATCH_H, PATCH_W = 301, 302\nSTRIDE_H, STRIDE_W = 73, 134\n\n# 模型啟用開關\n# USE_MASKRCNN = [True, True, True, True, True]\nUSE_MASKRCNN = [False, False, False, False, False]\nUSE_MASKRCNN_PATCH = True\nUSE_CELLPOSE = [True, True, True, True, True]\n# USE_CELLPOSE = [False, False, False, False, False]\n\n# 權重參數（允許之後調整）\nMASKRCNN_PER_FOLD_WEIGHT = [1.0, 1.0, 1.0, 1.0, 1.0]\nCELLPOSE_PER_FOLD_WEIGHT = [10.0, 1.0, 1.0, 1.0, 1.0]\nFUSION_MASKRCNN_WEIGHT = 1.0\nFUSION_CELLPOSE_WEIGHT = 1.0\n\nfor img_path in tqdm(test_files, desc=\"🚀 推論中\"):\n    img_id = img_path.stem\n    img = io.imread(img_path)\n\n    if img.ndim == 2:\n        img_rgb = np.stack([img, img, img], axis=-1)\n    elif img.ndim == 3 and img.shape[2] == 4:\n        img_rgb = img[..., :3]\n    else:\n        img_rgb = img\n\n    original_shape = img_rgb.shape[:2]\n    maskrcnn_probs, maskrcnn_dps, maskrcnn_total_weight = [], [], 0.0\n    cellpose_probs, cellpose_dps, cellpose_total_weight = [], [], 0.0\n\n    if USE_MASKRCNN:\n        for i in range(5):\n            if not USE_MASKRCNN[i]:\n                continue\n            cfg.MODEL.WEIGHTS = f\"/kaggle/input/final-maskrcnn-5fold-models/fold{i+1}/best_model.pth\"\n            predictor = DefaultPredictor(cfg)\n            if USE_MASKRCNN_PATCH:\n                prob, dp = predict_with_maskrcnn_patching(predictor, img_rgb, img_id, (PATCH_H, PATCH_W), (STRIDE_H, STRIDE_W))\n            else:\n                prob, dp = tta_predict_probability_and_flows(predictor, img_rgb, img_id, model_type=\"maskrcnn\")\n            maskrcnn_probs.append(prob * MASKRCNN_PER_FOLD_WEIGHT[i])\n            maskrcnn_dps.append(dp * MASKRCNN_PER_FOLD_WEIGHT[i])\n            maskrcnn_total_weight += MASKRCNN_PER_FOLD_WEIGHT[i]\n\n    if USE_CELLPOSE:\n        for i in range(5):\n            if not USE_CELLPOSE[i]:\n                continue\n            model = models.CellposeModel(gpu=True, pretrained_model=f\"/kaggle/input/final-cellpose-5fold-models/fold{i+1}/best_model.pth\")\n            prob, dp = tta_predict_probability_and_flows(model, img_rgb, img_id, model_type=\"cellpose\")\n            cellpose_probs.append(prob * CELLPOSE_PER_FOLD_WEIGHT[i])\n            cellpose_dps.append(dp * CELLPOSE_PER_FOLD_WEIGHT[i])\n            cellpose_total_weight += CELLPOSE_PER_FOLD_WEIGHT[i]\n\n    fused_prob_map = np.zeros(original_shape, dtype=np.float32)\n    fused_dp_map = np.zeros((2, *original_shape), dtype=np.float32)\n\n    if maskrcnn_total_weight > 0:\n        avg_maskrcnn_prob = np.sum(maskrcnn_probs, axis=0) / maskrcnn_total_weight\n        avg_maskrcnn_dp = np.sum(maskrcnn_dps, axis=0) / maskrcnn_total_weight\n\n        # 這裡是因為 cellpose 出來的 dp 都會是 5 倍\n        avg_maskrcnn_dp *= 5.0\n        \n        fused_prob_map += FUSION_MASKRCNN_WEIGHT * avg_maskrcnn_prob\n        fused_dp_map += FUSION_MASKRCNN_WEIGHT * avg_maskrcnn_dp\n\n    if cellpose_total_weight > 0:\n        avg_cellpose_prob = np.sum(cellpose_probs, axis=0) / cellpose_total_weight\n        avg_cellpose_dp = np.sum(cellpose_dps, axis=0) / cellpose_total_weight\n        fused_prob_map += FUSION_CELLPOSE_WEIGHT * avg_cellpose_prob\n        fused_dp_map += FUSION_CELLPOSE_WEIGHT * avg_cellpose_dp\n\n    total_fusion_weight = FUSION_MASKRCNN_WEIGHT * (maskrcnn_total_weight > 0) + FUSION_CELLPOSE_WEIGHT * (cellpose_total_weight > 0)\n    if total_fusion_weight > 0:\n        fused_prob_map /= total_fusion_weight\n        fused_dp_map /= total_fusion_weight\n    else:\n        submission_data.append({'id': img_id, 'predicted': ''})\n        continue\n\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    combined_mask = dynamics.resize_and_compute_masks(\n        fused_dp_map,\n        fused_prob_map,\n        cellprob_threshold=0.0,\n        flow_threshold=0.4,\n        min_size=20,\n        device=device\n    )\n\n    if combined_mask.max() > 0:\n        instance_ids = np.unique(combined_mask)\n        instance_ids = instance_ids[instance_ids != 0]\n        for inst_id in instance_ids:\n            binary_mask = (combined_mask == inst_id).astype(np.uint8)\n            if binary_mask.sum() >= 25:\n                rle = rle_encode(binary_mask)\n                submission_data.append({'id': img_id, 'predicted': rle})\n    else:\n        submission_data.append({'id': img_id, 'predicted': ''})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:16:57.341998Z","iopub.execute_input":"2025-06-01T14:16:57.342282Z","iopub.status.idle":"2025-06-01T14:17:47.273328Z","shell.execute_reply.started":"2025-06-01T14:16:57.342262Z","shell.execute_reply":"2025-06-01T14:17:47.272662Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Convert to the submission csv","metadata":{}},{"cell_type":"code","source":"submission_df = pd.DataFrame(submission_data)\nsubmission_df.to_csv('submission.csv', index=False)\n\n# ✅ 檢查\nprint(submission_df.head())\nprint(\"欄位名稱:\", submission_df.columns)\nprint(\"是否有 null:\", submission_df.isnull().sum())\nprint(\"是否有空字串 predicted:\", (submission_df['predicted'].astype(str).str.strip() == '').sum())\nprint(\"是否有 duplicated id + predicted:\", submission_df.duplicated(subset=[\"id\", \"predicted\"]).any())\nprint(\"id 總數:\", submission_df['id'].nunique(), \"submission 行數:\", len(submission_df))\nprint(\"predicted 欄型別:\", submission_df['predicted'].apply(type).value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:17:54.112287Z","iopub.execute_input":"2025-06-01T14:17:54.112898Z","iopub.status.idle":"2025-06-01T14:17:54.128991Z","shell.execute_reply.started":"2025-06-01T14:17:54.112873Z","shell.execute_reply":"2025-06-01T14:17:54.128272Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Visualization the final result","metadata":{}},{"cell_type":"code","source":"import random\nimport numpy as np\n\ndef random_color(seed=None):\n    if seed is not None:\n        random.seed(seed)\n    return [random.randint(0, 255) for _ in range(3)]\n\ndef mask_to_color(mask):\n    \"\"\"\n    將多個 instance mask 合成彩色圖像。\n    mask: numpy array, shape = (N, H, W)\n    return: RGB 彩色 mask，shape = (H, W, 3)\n    \"\"\"\n    if mask.ndim == 2:\n        mask = mask[np.newaxis, ...]  # 單一 mask 也包成 (1, H, W)\n\n    h, w = mask.shape[1:]\n    color_mask = np.zeros((h, w, 3), dtype=np.uint8)\n    for i in range(mask.shape[0]):\n        color = random_color(seed=i)\n        color_mask[mask[i] > 0] = color\n    return color_mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:18:00.147897Z","iopub.execute_input":"2025-06-01T14:18:00.148583Z","iopub.status.idle":"2025-06-01T14:18:00.153935Z","shell.execute_reply.started":"2025-06-01T14:18:00.148562Z","shell.execute_reply":"2025-06-01T14:18:00.153041Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"visualized_ids = submission_df['id'].unique()\n\nfor img_id in visualized_ids:\n    img_path = test_dir / f\"{img_id}.png\"\n    image = cv2.imread(str(img_path))\n\n    masks_rle = submission_df[submission_df['id'] == img_id]['predicted'].tolist()\n    decoded_masks = []\n\n    for rle in masks_rle:\n        if isinstance(rle, str) and rle.strip() != '':\n            decoded = rle_decode(rle, shape=(520, 704))\n            decoded_masks.append(decoded)\n\n    if len(decoded_masks) == 0:\n        print(f\"⚠️ {img_id} 沒有預測到 mask\")\n        continue\n\n    instance_masks = np.array(decoded_masks)\n    colored_mask = mask_to_color(instance_masks)\n\n    plt.figure(figsize=(15, 15))\n    plt.imshow(image[..., ::-1])\n    plt.imshow(colored_mask, alpha=0.5)\n    plt.axis(\"off\")\n    plt.title(f\"Image {img_id} — Loaded from submission.csv\", fontsize=20)\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T14:18:02.700463Z","iopub.execute_input":"2025-06-01T14:18:02.701183Z","iopub.status.idle":"2025-06-01T14:18:04.935318Z","shell.execute_reply.started":"2025-06-01T14:18:02.701157Z","shell.execute_reply":"2025-06-01T14:18:04.934541Z"}},"outputs":[],"execution_count":null}]}