{"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":2724590,"sourceType":"datasetVersion","datasetId":1660631},{"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-02T14:12:28.209549Z","iopub.execute_input":"2025-06-02T14:12:28.210285Z","iopub.status.idle":"2025-06-02T14:12:29.760788Z","shell.execute_reply.started":"2025-06-02T14:12:28.210260Z","shell.execute_reply":"2025-06-02T14:12:29.759874Z"}},"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-02T14:12:29.762665Z","iopub.execute_input":"2025-06-02T14:12:29.762947Z","iopub.status.idle":"2025-06-02T14:12:31.227549Z","shell.execute_reply.started":"2025-06-02T14:12:29.762922Z","shell.execute_reply":"2025-06-02T14:12:31.226618Z"}},"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 *\nfrom skimage.color import label2rgb\nfrom detectron2.structures import Boxes, Instances\ndetectron2.__version__\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-02T14:12:31.228749Z","iopub.execute_input":"2025-06-02T14:12:31.228986Z","iopub.status.idle":"2025-06-02T14:12:31.236361Z","shell.execute_reply.started":"2025-06-02T14:12:31.228964Z","shell.execute_reply":"2025-06-02T14:12:31.235602Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Prepare model in advance","metadata":{}},{"cell_type":"code","source":"# Mask R-CNN 五折模型的檔案路徑\nmaskrcnn_weights = [\n    # '/kaggle/input/final-maskrcnn-5fold-models/fold1/best_model.pth',\n    # '/kaggle/input/final-maskrcnn-5fold-models/fold2/best_model.pth',\n    # '/kaggle/input/final-maskrcnn-5fold-models/fold3/best_model.pth',\n    '/kaggle/input/final-maskrcnn-5fold-models/fold4/best_model.pth',\n    # '/kaggle/input/final-maskrcnn-5fold-models/fold5/best_model.pth',\n    # '/kaggle/input/detectron2-models-v2/model_0000999.pth'\n]\n\n# 每個模型對應的權重 (如需自定)\nMASKRCNN_PER_FOLD_WEIGHT = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]\n\n# 初始化所有 predictor，使用相同設定 (可依需求修改 config)\ndef load_maskrcnn_predictor(weight_path):\n    cfg = get_cfg()\n    cfg.merge_from_file(model_zoo.get_config_file(\"COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml\"))\n    cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.3\n    cfg.MODEL.ROI_HEADS.NUM_CLASSES = 3\n    cfg.MODEL.DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n    cfg.MODEL.WEIGHTS = weight_path\n    return DefaultPredictor(cfg)\n\n# 建立 Mask R-CNN 模型列表\nmaskrcnn_models_list = [\n    load_maskrcnn_predictor(w) for w in maskrcnn_weights\n]\n\nprint(f\"Loaded {len(maskrcnn_models_list)} Mask R-CNN models for ensemble.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:31.238018Z","iopub.execute_input":"2025-06-02T14:12:31.238218Z","iopub.status.idle":"2025-06-02T14:12:34.113684Z","shell.execute_reply.started":"2025-06-02T14:12:31.238203Z","shell.execute_reply":"2025-06-02T14:12:34.112900Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cellpose Ensemble 模型路徑\ncellpose_weight_paths = [\n    # '/kaggle/input/final-cellpose-5fold-models/fold1/best_model.pth',\n    # '/kaggle/input/final-cellpose-5fold-models/fold2/best_model.pth',\n    # '/kaggle/input/final-cellpose-5fold-models/fold3/best_model.pth',\n    # '/kaggle/input/final-cellpose-5fold-models/fold4/best_model.pth',\n    # '/kaggle/input/final-cellpose-5fold-models/fold5/best_model.pth',\n    '/kaggle/input/my-cellpose-models/best_model_0.30468207597732544.pth'\n]\n\n# 權重比例（可以根據 mAP 手動設定）\nCELLPOSE_PER_FOLD_WEIGHT = [1.0, 1.0]\n\n# 初始化 Cellpose 模型\ncellpose_models_list = [\n    models.CellposeModel(gpu=True, pretrained_model=weight_path)\n    for weight_path in cellpose_weight_paths\n]\n\n\nprint(f\"Loaded {len(cellpose_models_list)} Cellpose models for ensemble.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:34.114468Z","iopub.execute_input":"2025-06-02T14:12:34.114839Z","iopub.status.idle":"2025-06-02T14:12:37.533289Z","shell.execute_reply.started":"2025-06-02T14:12:34.114820Z","shell.execute_reply":"2025-06-02T14:12:37.532487Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# helper function","metadata":{}},{"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-02T14:12:37.534198Z","iopub.execute_input":"2025-06-02T14:12:37.534480Z","iopub.status.idle":"2025-06-02T14:12:37.540948Z","shell.execute_reply.started":"2025-06-02T14:12:37.534455Z","shell.execute_reply":"2025-06-02T14:12:37.540070Z"}},"outputs":[],"execution_count":null},{"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-02T14:12:37.541777Z","iopub.execute_input":"2025-06-02T14:12:37.542095Z","iopub.status.idle":"2025-06-02T14:12:37.556361Z","shell.execute_reply.started":"2025-06-02T14:12:37.542072Z","shell.execute_reply":"2025-06-02T14:12:37.555849Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pycocotools.mask\n\ndef remove_overlap_naive(masks):\n    \"\"\"\n    Greedy pixel‐level removal for overlapping masks：\n      先把每個 mask 編成 RLE、計算 IoU，再對同一 cluster 的 masks 以貪婪方式移除重疊部分。\n    Args:\n        masks (np.ndarray): shape = (N, H, W)，二值 mask（uint8 或 bool）。\n    Returns:\n        np.ndarray: 經過貪婪重疊移除後的 masks，shape = (M, H, W)，M ≤ N。\n    \"\"\"\n    if masks.size == 0:\n        return masks\n\n    rles = [pycocotools.mask.encode(np.asfortranarray(m.astype(np.uint8))) for m in masks]\n    ious = pycocotools.mask.iou(rles, rles, [0] * len(rles))\n    np.fill_diagonal(ious, 0)\n\n    toproc = np.where(ious.sum(axis=0) > 0)[0]\n    if len(toproc) == 0:\n        return masks\n\n    mt = torch.from_numpy(masks.astype(np.uint8)).cuda()\n    prev = mt[toproc[0]].clone()\n    for idx, i in enumerate(toproc[1:], start=1):\n        prev = torch.max(prev, mt[toproc[idx - 1]])\n        mt[i] *= (~prev)\n    return mt.cpu().numpy()\n\ndef instmap_to_masks_boxes(inst_map):\n    \"\"\"\n    Convert 2D instance label map → 二值 masks + boxes (x0,y0,x1,y1,area, dummy_score=0)  \n    Args:\n        inst_map (np.ndarray): shape = (H, W)，稠密 instance map，0=背景，1,2,3...=各實例\n    Returns:\n        masks (np.ndarray)：shape = (K, H, W)，若無任何實例回傳 (None, None)\n        boxes (np.ndarray)：shape = (K, 6)，每行 [x0, y0, x1, y1, area, 0.0]\n    \"\"\"\n    masks, boxes = [], []\n    for lab in np.unique(inst_map)[1:]:\n        m = (inst_map == lab).astype(np.uint8)\n        ys, xs = np.where(m)\n        if ys.size == 0:\n            continue\n        area = int(m.sum())\n        masks.append(m)\n        boxes.append([xs.min(), ys.min(), xs.max(), ys.max(), area, 0.0])\n    if not masks:\n        return None, None\n    return np.stack(masks, axis=0), np.array(boxes, dtype=float)\n\ndef weighted_mask_fusion_nmw(masks, boxes, scores, iou_thr=0.15, score_coef=0.8):\n    N = masks.shape[0]\n    if N == 0:\n        return np.zeros((0, masks.shape[1], masks.shape[2]), dtype=np.uint8), np.zeros((0, 6), dtype=float)\n\n    rles = [pycocotools.mask.encode(np.asfortranarray(m)) for m in masks]\n    ious = pycocotools.mask.iou(rles, rles, [0] * N)\n    used = set()\n    fused_masks, fused_boxes = [], []\n\n    for i in range(N):\n        if i in used:\n            continue\n        group = [i]\n        for j in range(i + 1, N):\n            if ious[i, j] > iou_thr:\n                group.append(j)\n        used.update(group)\n\n        sub_masks = masks[group]\n        sub_boxes = boxes[group]\n        sub_scores = scores[group].astype(float) * score_coef\n\n        if len(group) == 1:\n            fused_masks.append(sub_masks[0])\n            fused_boxes.append(sub_boxes[0])\n            continue\n\n        weights = sub_scores / sub_scores.sum()\n        soft_map = np.tensordot(weights, sub_masks, axes=(0, 0))\n        bin_mask = (soft_map >= 0.5).astype(np.uint8)\n\n        ys, xs = np.where(bin_mask)\n        if ys.size == 0:\n            best_idx = group[np.argmax(sub_scores)]\n            bin_mask = masks[best_idx]\n            ys, xs = np.where(bin_mask)\n            fused_masks.append(bin_mask)\n            fused_boxes.append([xs.min(), ys.min(), xs.max(), ys.max(), int(bin_mask.sum()), 0.0])\n        else:\n            fused_masks.append(bin_mask)\n            fused_boxes.append([xs.min(), ys.min(), xs.max(), ys.max(), int(bin_mask.sum()), 0.0])\n\n    if not fused_masks:\n        return np.zeros((0, masks.shape[1], masks.shape[2]), dtype=np.uint8), np.zeros((0, 6), dtype=float)\n    return np.stack(fused_masks, axis=0), np.array(fused_boxes, dtype=float)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:37.557196Z","iopub.execute_input":"2025-06-02T14:12:37.557420Z","iopub.status.idle":"2025-06-02T14:12:37.576595Z","shell.execute_reply.started":"2025-06-02T14:12:37.557404Z","shell.execute_reply":"2025-06-02T14:12:37.575961Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -----------------------------\n# DIoU 計算\n# -----------------------------\ndef compute_diou(boxes1, boxes2):\n    # 保證只取前 4 個座標\n    boxes1 = boxes1[:, :4]\n    boxes2 = boxes2[:, :4]\n\n    N, M = boxes1.shape[0], boxes2.shape[0]\n    diou = np.zeros((N, M), dtype=np.float32)\n\n    for i in range(N):\n        x1_1, y1_1, x2_1, y2_1 = boxes1[i]\n        area1 = (x2_1 - x1_1) * (y2_1 - y1_1)\n        c_x1_1, c_y1_1 = (x1_1 + x2_1) / 2, (y1_1 + y2_1) / 2\n\n        for j in range(M):\n            x1_2, y1_2, x2_2, y2_2 = boxes2[j]\n            area2 = (x2_2 - x1_2) * (y2_2 - y1_2)\n\n            inter_x1 = max(x1_1, x1_2)\n            inter_y1 = max(y1_1, y1_2)\n            inter_x2 = min(x2_1, x2_2)\n            inter_y2 = min(y2_1, y2_2)\n            inter_area = max(0, inter_x2 - inter_x1) * max(0, inter_y2 - inter_y1)\n\n            union_area = area1 + area2 - inter_area\n            iou = inter_area / (union_area + 1e-6)\n\n            c_x1_2, c_y1_2 = (x1_2 + x2_2) / 2, (y1_2 + y2_2) / 2\n            center_dist = (c_x1_1 - c_x1_2) ** 2 + (c_y1_1 - c_y1_2) ** 2\n\n            enclose_x1 = min(x1_1, x1_2)\n            enclose_y1 = min(y1_1, y1_2)\n            enclose_x2 = max(x2_1, x2_2)\n            enclose_y2 = max(y2_1, y2_2)\n            enclose_diag = (enclose_x2 - enclose_x1) ** 2 + (enclose_y2 - enclose_y1) ** 2\n\n            diou[i, j] = iou - center_dist / (enclose_diag + 1e-6)\n\n    return diou\n\n# -----------------------------\n# Weighted Cluster-NMS with DIoU\n# -----------------------------\ndef weighted_mask_fusion_diou_nms(masks, boxes, scores, iou_thr=0.15, overlap_thr=0.2):\n    if len(masks) == 0:\n        return np.zeros((0, *masks.shape[1:]), dtype=np.uint8), np.zeros((0, 4), dtype=np.float32), np.array([])\n\n    indices = np.argsort(-scores)\n    boxes = boxes[indices]\n    masks = masks[indices]\n    scores = scores[indices]\n\n    keep_masks, keep_boxes, keep_scores = [], [], []\n    used = np.zeros(len(scores), dtype=bool)\n\n    for i in range(len(scores)):\n        if used[i]:\n            continue\n\n        ref_mask = masks[i].astype(np.float32)\n        ref_box = boxes[i][:4]  # 只取前 4 維 box\n        ref_score = scores[i]\n\n        group = [i]\n        weights = [ref_score]\n        sum_mask = ref_mask * ref_score\n\n        diou_scores = compute_diou(np.expand_dims(ref_box, 0), boxes[:, :4])[0]\n        for j in range(i + 1, len(scores)):\n            if used[j]:\n                continue\n            if diou_scores[j] > iou_thr:\n                group.append(j)\n                weights.append(scores[j])\n                sum_mask += masks[j].astype(np.float32) * scores[j]\n                used[j] = True\n\n        fused_mask = (sum_mask / np.sum(weights)) > 0.5\n\n        discard = False\n        for kept in keep_masks:\n            intersect = np.logical_and(kept, fused_mask).sum()\n            if intersect / (fused_mask.sum() + 1e-6) > overlap_thr:\n                discard = True\n                break\n\n        if not discard:\n            keep_masks.append(fused_mask.astype(np.uint8))\n            keep_boxes.append(ref_box)\n            keep_scores.append(ref_score)\n\n    return np.stack(keep_masks), np.stack(keep_boxes), np.array(keep_scores)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:37.577250Z","iopub.execute_input":"2025-06-02T14:12:37.577454Z","iopub.status.idle":"2025-06-02T14:12:37.591237Z","shell.execute_reply.started":"2025-06-02T14:12:37.577440Z","shell.execute_reply":"2025-06-02T14:12:37.590520Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport cv2\nimport torch\nimport pycocotools.mask\nfrom skimage.measure import label, regionprops\n\n# -----------------------------\n# Resize Image\n# -----------------------------\ndef resize_image(image, target_h):\n    scale = target_h / image.shape[0]\n    target_w = int(image.shape[1] * scale)\n    resized = cv2.resize(image, (target_w, target_h), interpolation=cv2.INTER_LINEAR)\n    return resized, scale\n\n# -----------------------------\n# Patch Extraction\n# -----------------------------\ndef get_image_patches(image, patch_h, patch_w, stride_h, stride_w):\n    H, W = image.shape[:2]\n    patches, coords = [], []\n    for y in range(0, H - patch_h + 1, stride_h):\n        for x in range(0, W - patch_w + 1, stride_w):\n            patch = image[y:y + patch_h, x:x + patch_w]\n            patches.append(patch)\n            coords.append((y, x))\n    return patches, coords\n\n# -----------------------------\n# Paste patch-sized masks back to original image coordinates\n# -----------------------------\ndef paste_full_patch_masks(masks, patch_origin, scale_ratio, out_h, out_w):\n    py, px = patch_origin\n    canvas = np.zeros((len(masks), out_h, out_w), dtype=np.uint8)\n\n    for i, mask in enumerate(masks):\n        mask_rescaled = cv2.resize(mask.astype(np.uint8), None, fx=1/scale_ratio, fy=1/scale_ratio, interpolation=cv2.INTER_NEAREST)\n        h, w = mask_rescaled.shape\n        y0, x0 = int(py / scale_ratio), int(px / scale_ratio)\n        y1, x1 = y0 + h, x0 + w\n\n        y1 = min(y1, out_h)\n        x1 = min(x1, out_w)\n        canvas[i, y0:y1, x0:x1] = mask_rescaled[:y1 - y0, :x1 - x0]\n\n    return canvas\n\n# -----------------------------\n# Convert masks to boxes and classes\n# -----------------------------\ndef masks_to_boxes_classes(masks, classes):\n    boxes = []\n    final_classes = []\n    keep_idx = []\n    for i, mask in enumerate(masks):\n        if mask.sum() == 0:\n            continue\n        labeled = label(mask)\n        props = regionprops(labeled)\n        if not props:\n            continue\n        y0, x0, y1, x1 = props[0].bbox\n        area = int(mask.sum())\n        boxes.append([x0, y0, x1, y1, area, 0.0])\n        final_classes.append(classes[i])\n        keep_idx.append(i)\n    return np.array(boxes, dtype=float), np.array(final_classes, dtype=int), np.array(keep_idx, dtype=int)\n\ndef inverse_boxes(boxes, tta_type, image_shape):\n    h, w = image_shape\n    boxes = boxes.copy()\n\n    if tta_type == 'hflip':\n        boxes[:, 0], boxes[:, 2] = w - boxes[:, 2], w - boxes[:, 0]\n    elif tta_type == 'vflip':\n        boxes[:, 1], boxes[:, 3] = h - boxes[:, 3], h - boxes[:, 1]\n    elif tta_type == 'rot180':\n        boxes[:, 0], boxes[:, 2] = w - boxes[:, 2], w - boxes[:, 0]\n        boxes[:, 1], boxes[:, 3] = h - boxes[:, 3], h - boxes[:, 1]\n\n    return boxes\n\n# -----------------------------\n# Main inference logic\n# -----------------------------\ndef infer_maskrcnn_with_tta_patch_multi_scale(\n    image, predictor,\n    patch_size=(301, 302), stride=(73, 134),\n    scales=(440, 520, 620),\n    visualize=False\n):\n    all_masks, all_boxes, all_scores, all_classes = [], [], [], []\n\n    tta_transforms = [\n        lambda x: x\n    ]\n    inverse_transforms = [\n        lambda m: m\n    ]\n    tta_types = ['none']\n\n    for scale in scales:\n        resized_img, scale_ratio = resize_image(image, scale)\n\n        for tta_idx, transform in enumerate(tta_transforms):\n            tta_type = tta_types[tta_idx]\n            inverse = inverse_transforms[tta_idx]\n            tta_img = transform(resized_img)\n            patches, coords = get_image_patches(tta_img, *patch_size, *stride)\n\n            for patch, (y, x) in zip(patches, coords):\n                m, b, s, c = infer_maskrcnn(patch, predictor)\n                if m is None or len(m) == 0:\n                    continue\n\n                b[:, [0, 2]] += x\n                b[:, [1, 3]] += y\n                b = b / scale_ratio\n                b = inverse_boxes(b, tta_type, image.shape[:2])\n\n                masks_full = paste_full_patch_masks(\n                    masks=m,\n                    patch_origin=(y, x),\n                    scale_ratio=scale_ratio,\n                    out_h=image.shape[0],\n                    out_w=image.shape[1]\n                )\n\n                masks_full = inverse(masks_full)\n\n                resized = np.zeros((masks_full.shape[0], image.shape[0], image.shape[1]), dtype=np.uint8)\n                for i in range(masks_full.shape[0]):\n                    resized[i] = cv2.resize(\n                        masks_full[i].astype(np.uint8),\n                        (image.shape[1], image.shape[0]),\n                        interpolation=cv2.INTER_NEAREST\n                    )\n                masks_full = resized\n\n                if visualize:\n                    visualize_maskrcnn_prediction(image, masks_full, b, s)\n\n                all_masks.append(masks_full)\n                all_boxes.append(b)\n                all_scores.append(s)\n                all_classes.append(c)\n\n    if not all_masks:\n        return None, None, None, None\n\n    masks_concat = np.concatenate(all_masks, axis=0)\n    boxes_concat = np.concatenate(all_boxes, axis=0)\n    scores_concat = np.concatenate(all_scores, axis=0)\n    classes_concat = np.concatenate(all_classes, axis=0)\n\n    fused_masks, _, fused_scores = weighted_mask_fusion_diou_nms(\n        masks_concat, boxes_concat, scores_concat,\n        iou_thr=0.15, overlap_thr=0.2\n    )\n\n    if fused_masks.shape[0] == 0:\n        return None, None, None, None\n\n    fused_boxes, fused_classes, keep_idx = masks_to_boxes_classes(fused_masks, classes_concat)\n\n    fused_masks = fused_masks[keep_idx]\n    fused_scores = fused_scores[keep_idx]\n\n    if visualize:\n        visualize_maskrcnn_prediction(image, fused_masks, fused_boxes, fused_scores)\n\n    return fused_masks, fused_boxes, fused_scores, fused_classes\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:37.593755Z","iopub.execute_input":"2025-06-02T14:12:37.593951Z","iopub.status.idle":"2025-06-02T14:12:37.614830Z","shell.execute_reply.started":"2025-06-02T14:12:37.593937Z","shell.execute_reply":"2025-06-02T14:12:37.614197Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import numpy as np\n# import cv2\n# import torch\n# import pycocotools.mask\n\n# # -----------------------------\n# # Resize Image\n# # -----------------------------\n# def resize_image(image, target_h):\n#     scale = target_h / image.shape[0]\n#     target_w = int(image.shape[1] * scale)\n#     resized = cv2.resize(image, (target_w, target_h), interpolation=cv2.INTER_LINEAR)\n#     return resized, scale\n\n# # -----------------------------\n# # Patch Extraction\n# # -----------------------------\n# def get_image_patches(image, patch_h, patch_w, stride_h, stride_w):\n#     H, W = image.shape[:2]\n#     patches, coords = [], []\n#     for y in range(0, H - patch_h + 1, stride_h):\n#         for x in range(0, W - patch_w + 1, stride_w):\n#             patch = image[y:y + patch_h, x:x + patch_w]\n#             patches.append(patch)\n#             coords.append((y, x))\n#     return patches, coords\n\n# # -----------------------------\n# # Paste patch-sized masks back to original image coordinates\n# # -----------------------------\n# def paste_full_patch_masks(masks, patch_origin, scale_ratio, out_h, out_w):\n#     py, px = patch_origin\n#     canvas = np.zeros((len(masks), out_h, out_w), dtype=np.uint8)\n\n#     for i, mask in enumerate(masks):\n#         mask_rescaled = cv2.resize(mask.astype(np.uint8), None, fx=1/scale_ratio, fy=1/scale_ratio, interpolation=cv2.INTER_NEAREST)\n#         h, w = mask_rescaled.shape\n#         y0, x0 = int(py / scale_ratio), int(px / scale_ratio)\n#         y1, x1 = y0 + h, x0 + w\n\n#         y1 = min(y1, out_h)\n#         x1 = min(x1, out_w)\n#         canvas[i, y0:y1, x0:x1] = mask_rescaled[:y1 - y0, :x1 - x0]\n\n#     return canvas\n\n# # -----------------------------\n# # Filter boxes near patch center\n# # -----------------------------\n# def filter_boxes_near_patch_center(boxes, patch_size, margin=40):\n#     ph, pw = patch_size\n#     keep_indices = []\n#     for i, box in enumerate(boxes):\n#         x0, y0, x1, y1 = box[:4]\n#         cx = (x0 + x1) / 2\n#         cy = (y0 + y1) / 2\n#         if margin < cx < (pw - margin) and margin < cy < (ph - margin):\n#             keep_indices.append(i)\n#     return keep_indices\n\n# # -----------------------------\n# # Re-TTA for boxes\n# # -----------------------------\n# def retta_boxes(boxes, tta_type, img_height, img_width):\n#     boxes_flipped = boxes.copy()\n\n#     if tta_type == 'hflip':\n#         boxes_flipped[:, 0] = img_width - boxes[:, 2]\n#         boxes_flipped[:, 2] = img_width - boxes[:, 0]\n\n#     elif tta_type == 'vflip':\n#         boxes_flipped[:, 1] = img_height - boxes[:, 3]\n#         boxes_flipped[:, 3] = img_height - boxes[:, 1]\n\n#     elif tta_type == 'rot180':\n#         boxes_flipped[:, 0] = img_width - boxes[:, 2]\n#         boxes_flipped[:, 2] = img_width - boxes[:, 0]\n#         boxes_flipped[:, 1] = img_height - boxes[:, 3]\n#         boxes_flipped[:, 3] = img_height - boxes[:, 1]\n\n#     return boxes_flipped\n\n# # -----------------------------\n# # Main inference logic\n# # -----------------------------\n# def infer_maskrcnn_with_tta_patch_multi_scale(\n#     image, predictor,\n#     patch_size=(301, 302), stride=(73, 134),\n#     scales=(440, 480, 520, 560, 580, 620),\n#     center_margin=30, min_mask_area=100,\n#     visualize=False\n# ):\n#     all_masks, all_boxes, all_scores = [], [], []\n\n#     # 定義四種 TTA + 對應反變換\n#     tta_transforms = [\n#         lambda x: x,\n#         lambda x: np.flip(x, axis=1),\n#         lambda x: np.flip(x, axis=0),\n#         lambda x: np.rot90(x, k=2),\n#     ]\n#     inverse_transforms = [\n#         lambda m: m,\n#         lambda m: np.flip(m, axis=2),\n#         lambda m: np.flip(m, axis=1),\n#         lambda m: np.rot90(m, k=2, axes=(1, 2)),\n#     ]\n#     tta_types = ['none', 'hflip', 'vflip', 'rot180']\n\n#     for scale in scales:\n#         resized_img, scale_ratio = resize_image(image, scale)\n\n#         for tta_idx, transform in enumerate(tta_transforms):\n#             inverse = inverse_transforms[tta_idx]\n#             tta_type = tta_types[tta_idx]\n#             tta_img = transform(resized_img)\n#             patches, coords = get_image_patches(tta_img, *patch_size, *stride)\n\n#             for patch, (y, x) in zip(patches, coords):\n#                 m, b, s = infer_maskrcnn(patch, predictor)\n#                 if m is None or len(m) == 0:\n#                     continue\n\n#                 b[:, [0, 2]] += x\n#                 b[:, [1, 3]] += y\n\n#                 b_rescaled = b / scale_ratio\n#                 areas = (b_rescaled[:, 2] - b_rescaled[:, 0]) * (b_rescaled[:, 3] - b_rescaled[:, 1])\n#                 keep_large = areas > 20\n#                 m, b_rescaled, s = m[keep_large], b_rescaled[keep_large], s[keep_large]\n\n#                 score_thresh = 0.7\n#                 keep_high_score = s >= score_thresh\n#                 m, b_rescaled, s = m[keep_high_score], b_rescaled[keep_high_score], s[keep_high_score]\n#                 if len(m) == 0:\n#                     continue\n\n#                 img_height, img_width = image.shape[:2]\n#                 b_rescaled = retta_boxes(b_rescaled, tta_type, img_height=img_height, img_width=img_width)\n\n#                 masks_full = paste_full_patch_masks(\n#                     masks=m,\n#                     patch_origin=(y, x),\n#                     scale_ratio=scale_ratio,\n#                     out_h=image.shape[0],\n#                     out_w=image.shape[1]\n#                 )\n\n#                 # 反變換 (還原 TTA 變形)\n#                 masks_full = inverse(masks_full)\n\n#                 # 強制 resize to (H, W)，避免 concat 炸掉\n#                 resized = np.zeros((masks_full.shape[0], image.shape[0], image.shape[1]), dtype=np.uint8)\n#                 for i in range(masks_full.shape[0]):\n#                     resized[i] = cv2.resize(\n#                         masks_full[i].astype(np.uint8),\n#                         (image.shape[1], image.shape[0]),\n#                         interpolation=cv2.INTER_NEAREST\n#                     )\n#                 masks_full = resized\n\n#                 if visualize:\n#                     visualize_maskrcnn_prediction(image, masks_full, b_rescaled, s)\n\n#                 all_masks.append(masks_full)\n#                 all_boxes.append(b_rescaled)\n#                 all_scores.append(s)\n\n#     if not all_masks:\n#         return None, None, None\n\n#     masks_concat = np.concatenate(all_masks, axis=0)\n#     boxes_concat = np.concatenate(all_boxes, axis=0)\n#     scores_concat = np.concatenate(all_scores, axis=0)\n\n#     fused_masks, fused_boxes = weighted_mask_fusion_nmw(\n#         masks_concat, boxes_concat, scores_concat, iou_thr=0.15, score_coef=0.8\n#     )\n\n#     if fused_masks.shape[0] == 0:\n#         return None, None, None\n\n#     areas = fused_masks.sum(axis=(1, 2))\n#     keep = np.where(areas > min_mask_area)[0]\n#     fused_masks = fused_masks[keep]\n#     fused_boxes = fused_boxes[keep]\n#     fused_scores = scores_concat[keep]\n\n#     if visualize:\n#         visualize_maskrcnn_prediction(image, fused_masks, fused_boxes, fused_scores)\n\n#     return fused_masks, fused_boxes, fused_scores","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:37.615617Z","iopub.execute_input":"2025-06-02T14:12:37.615854Z","iopub.status.idle":"2025-06-02T14:12:37.633205Z","shell.execute_reply.started":"2025-06-02T14:12:37.615832Z","shell.execute_reply":"2025-06-02T14:12:37.632580Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def tta_predict_probability_and_flows(model, image, image_id):\n    \"\"\"\n    使用測試時間增強 (TTA) 來預測機率圖和流向量。\n    包括原始圖像、水平翻轉、垂直翻轉、水平垂直翻轉，\n    以及 90 度、180 度、270 度旋轉。\n\n    Args:\n        model: Cellpose 模型實例。\n        image (np.ndarray): 輸入的原始圖像。\n        image_id (str): 圖像的 ID (用於傳遞，本函數中未使用)。\n\n    Returns:\n        tuple: (averaged_prob_map, averaged_dp_map)\n               - averaged_prob_map (np.ndarray): 平均後的細胞機率圖。\n               - averaged_dp_map (np.ndarray): 平均後的流向量圖 (2, H, W)。\n    \"\"\"\n    images = [\n        image,                                       # 原圖\n        np.flip(image, axis=1)\n    ]\n    \n    # Prob map 對應的 inverse operation\n    inverse_ops_prob = [\n        lambda x: x,\n        lambda x: np.flip(x, axis=1)\n    ]\n    \n    # Flow map 對應的 inverse operation（注意順序）\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    ]\n\n    all_prob_maps = []\n    all_dp_maps = []\n    \n    # 獲取圖像的原始 H, W (對於 np.rot90 會改變 H, W 的順序，但我們處理的是原始圖像尺寸)\n    # Cellpose 的 eval_flows 應該會根據圖像的實際尺寸來處理\n    # 不過為了保險，可以在 `inverse_ops_dp` 裡面對 `np.rot90` 的結果進行尺寸調整\n    # 但 Cellpose 內部通常會處理輸入圖像的尺寸變化，並輸出正確尺寸的流場，\n    # 所以這裡主要確保逆變換的邏輯正確。\n    H, W = image.shape[:2] \n\n    for idx, aug_img in enumerate(images):\n        _, eval_flows, _ = model.eval(aug_img, compute_masks=False)\n        \n        pred_dp_map_raw = eval_flows[1].astype(np.float32)\n        pred_prob_map_raw = eval_flows[2].astype(np.float32)\n\n        # 執行機率圖的逆變換\n        pred_prob_map = inverse_ops_prob[idx](pred_prob_map_raw)\n        \n        # 執行流向量圖的逆變換\n        pred_dp_map_transformed = inverse_ops_dp[idx](pred_dp_map_raw)\n\n        if pred_prob_map.shape[:2] != (H, W):\n            pred_prob_map = cv2.resize(pred_prob_map, (W, H), interpolation=cv2.INTER_LINEAR)\n        if pred_dp_map_transformed.shape[1:] != (H, W):\n            pred_dp_map_transformed_x = cv2.resize(pred_dp_map_transformed[0], (W, H), interpolation=cv2.INTER_LINEAR)\n            pred_dp_map_transformed_y = cv2.resize(pred_dp_map_transformed[1], (W, H), interpolation=cv2.INTER_LINEAR)\n            pred_dp_map_transformed = np.stack([pred_dp_map_transformed_x, pred_dp_map_transformed_y], axis=0)\n\n\n        all_prob_maps.append(pred_prob_map)\n        all_dp_maps.append(pred_dp_map_transformed)\n\n    # 對所有增強圖像的預測結果進行平均\n    averaged_prob_map = np.mean(all_prob_maps, axis=0)\n    averaged_dp_map = np.mean(all_dp_maps, axis=0)\n    \n    return averaged_prob_map, averaged_dp_map","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:37.633861Z","iopub.execute_input":"2025-06-02T14:12:37.634017Z","iopub.status.idle":"2025-06-02T14:12:37.651646Z","shell.execute_reply.started":"2025-06-02T14:12:37.634005Z","shell.execute_reply":"2025-06-02T14:12:37.650900Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from detectron2.structures import Instances, Boxes\nimport torch\nfrom detectron2.utils.visualizer import Visualizer\n\ndef visualize_maskrcnn_prediction(image, masks, boxes, scores):\n    H, W = image.shape[:2]\n    instances = Instances((H, W))\n    instances.pred_masks = torch.tensor(masks).bool()\n    instances.pred_boxes = Boxes(torch.tensor(boxes[:, :4]))\n    instances.scores = torch.tensor(scores)\n\n    visualizer = Visualizer(image[:, :, ::-1], scale=1.0)\n    out = visualizer.draw_instance_predictions(instances)\n    plt.figure(figsize=(12, 12))\n    plt.imshow(out.get_image()[:, :, ::-1])\n    plt.axis(\"off\")\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:37.652414Z","iopub.execute_input":"2025-06-02T14:12:37.652681Z","iopub.status.idle":"2025-06-02T14:12:37.667784Z","shell.execute_reply.started":"2025-06-02T14:12:37.652657Z","shell.execute_reply":"2025-06-02T14:12:37.667063Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def infer_maskrcnn(image, predictor, score_thresh=[0.7, 0.7, 0.7]):\n    \"\"\"\n    使用 Detectron2 的 DefaultPredictor 推論，根據 class 對應的 score 門檻過濾。\n    Args:\n        image (np.ndarray): shape = (H, W, 3)\n        predictor: Detectron2 DefaultPredictor\n        score_thresh (list[float]): 每類 class 的 score 過濾門檻，例如 [0.7, 0.5, 0.3]\n    Returns:\n        masks (np.ndarray): shape = (N, H, W)\n        boxes (np.ndarray): shape = (N, 6) → [x0, y0, x1, y1, area, 0.0]\n        scores (np.ndarray): shape = (N,)\n        classes (np.ndarray): shape = (N,) → class index (int)\n    \"\"\"\n    outputs = predictor(image)\n    instances = outputs[\"instances\"].to(\"cpu\")  # ✅ 只最後再搬到 CPU，避免 GPU → CPU 早搬\n\n    if len(instances) == 0:\n        return None, None, None, None\n\n    pred_masks  = instances.pred_masks.numpy()              # (N, H, W)\n    pred_boxes  = instances.pred_boxes.tensor.numpy()       # (N, 4)\n    scores      = instances.scores.numpy()                  # (N,)\n    classes     = instances.pred_classes.numpy()            # (N,)\n\n    # ✅ 利用 np.where 快速取 index（避免 for loop）\n    keep = np.where([\n        cls < len(score_thresh) and scores[i] >= score_thresh[cls]\n        for i, cls in enumerate(classes)\n    ])[0]\n\n    if len(keep) == 0:\n        return None, None, None, None\n\n    pred_masks = pred_masks[keep]\n    pred_boxes = pred_boxes[keep]\n    scores     = scores[keep]\n    classes    = classes[keep]\n\n    areas = pred_masks.sum(axis=(1, 2)).astype(float)\n    boxes_with_extra = np.hstack([\n        pred_boxes,\n        areas[:, None],\n        np.zeros((len(areas), 1))  # fake score placeholder\n    ])\n\n    return pred_masks, boxes_with_extra, scores, classes","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:37.668505Z","iopub.execute_input":"2025-06-02T14:12:37.668713Z","iopub.status.idle":"2025-06-02T14:12:37.683944Z","shell.execute_reply.started":"2025-06-02T14:12:37.668700Z","shell.execute_reply":"2025-06-02T14:12:37.683341Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def ensemble_and_generate_submission(test_dir, output_csv='submission.csv', min_size=75):\n    test_files = sorted([f for f in Path(test_dir).iterdir() if f.suffix == '.png'])\n    submission_data = []\n\n    # 每個類別的 score 門檻與最小像素數\n    SCORE_THRESHOLDS = [0.7, 0.45, 0.4]\n    MIN_PIXELS = [80, 150, 60]\n\n    for img_path in tqdm(test_files, desc='推證中'):\n        img_id = img_path.stem\n        img = io.imread(img_path)\n        img_rgb = img if img.ndim == 3 and img.shape[2] == 3 else (\n            img[..., :3] if img.ndim == 3 and img.shape[2] == 4 else np.stack([img]*3, axis=-1)\n        )\n        H, W = img.shape[:2]\n        all_masks, all_boxes, all_scores, all_classes = [], [], [], []\n\n        # === Mask R-CNN Ensemble ===\n        for model in maskrcnn_models_list:\n            m, b, s, c = infer_maskrcnn(img_rgb, model)\n            if m is not None:\n                print(\"=================== Non-Patch ===================\")\n                visualize_maskrcnn_prediction(img_rgb, m, b, s)\n                all_masks.append(m)\n                all_boxes.append(b[:, :4])\n                all_scores.append(s)\n                all_classes.append(c)\n\n            m_, b_, s_, c_ = infer_maskrcnn_with_tta_patch_multi_scale(img_rgb, model)\n            if m_ is not None:\n                print(\"=================== Patch + TTA ===================\")\n                visualize_maskrcnn_prediction(img_rgb, m_, b_, s_)\n                all_masks.append(m_)\n                all_boxes.append(b_[:, :4])\n                all_scores.append(s_)\n                all_classes.append(c_)\n\n        # === Cellpose 加權平均之後再 decode ===\n        prob_map_sum, dp_map_sum, total_weight = None, None, 0.0\n        cp_device = 'cpu'\n        for i_model, cp_model in enumerate(cellpose_models_list):\n            prob_map, dp_map = tta_predict_probability_and_flows(cp_model, img_rgb, img_id)\n            weight = CELLPOSE_PER_FOLD_WEIGHT[i_model]\n\n            if prob_map_sum is None:\n                prob_map_sum = prob_map * weight\n                dp_map_sum = dp_map * weight\n            else:\n                prob_map_sum += prob_map * weight\n                dp_map_sum += dp_map * weight\n\n            total_weight += weight\n            cp_device = cp_model.device\n\n        if total_weight > 0:\n            avg_prob_map = prob_map_sum / total_weight\n            avg_dp_map = dp_map_sum / total_weight\n            print(\"=================== Cellpose ===================\")\n            new_dp = np.moveaxis(avg_dp_map, 0, -1)\n            visualize_flow(new_dp)\n\n            inst_map = dynamics.resize_and_compute_masks(\n                avg_dp_map, avg_prob_map, flow_threshold=0.4, min_size=20, device=cp_device)\n\n            if inst_map.max() > 0:\n                m_bin, bxs = instmap_to_masks_boxes(inst_map)\n                if m_bin is not None:\n                    scores = [float(avg_prob_map[mask.astype(bool)].mean()) if mask.sum() > 0 else 0.0 for mask in m_bin]\n                    all_masks.append(m_bin)\n                    all_boxes.append(bxs[:, :4])\n                    all_scores.append(np.array(scores))\n                    all_classes.append(np.zeros(len(scores), dtype=np.int32))  # Cellpose 類別固定為 0\n\n        # === 無任何預測情況 ===\n        if not all_masks:\n            submission_data.append({'id': img_id, 'predicted': ''})\n            continue\n\n        # === 合併所有預測 + 權重融合（改為 DIoU-NMS） ===\n        masks_concat = np.concatenate(all_masks, axis=0)\n        boxes_concat = np.concatenate(all_boxes, axis=0)\n        scores_concat = np.concatenate(all_scores, axis=0)\n        classes_concat = np.concatenate(all_classes, axis=0)\n\n        fused_masks, fused_boxes, fused_scores = weighted_mask_fusion_diou_nms(\n            masks_concat, boxes_concat, scores_concat\n        )\n\n        # 依據類別篩選 min_pixel + score threshold\n        if fused_masks.shape[0] > 0:\n            areas = fused_masks.sum(axis=(1, 2))\n            keep = []\n            for i in range(fused_masks.shape[0]):\n                cls = classes_concat[i]\n                score = fused_scores[i]\n                area = areas[i]\n                if score >= SCORE_THRESHOLDS[cls] and area >= MIN_PIXELS[cls]:\n                    keep.append(i)\n\n            if len(keep) == 0:\n                submission_data.append({'id': img_id, 'predicted': ''})\n                continue\n\n            fused_masks = fused_masks[keep]\n            fused_boxes = fused_boxes[keep]\n            fused_scores = fused_scores[keep]\n            classes_concat = classes_concat[keep]\n\n        # === Connected Components 後處理 + RLE 編碼 ===\n        canvas = np.zeros((H, W), dtype=np.int32)\n        instance_id = 1\n        for i, m in enumerate(fused_masks):\n            mask = m.astype(bool)\n            assign_area = mask.sum()\n            if assign_area < MIN_PIXELS[classes_concat[i]]:\n                continue\n\n            num_connected, _ = cv2.connectedComponents(mask.astype(np.uint8))\n            if num_connected > 2:\n                continue\n\n            overlap_ratio = (canvas & mask).sum() / assign_area if assign_area > 0 else 1.0\n            if overlap_ratio > 0.2:\n                continue\n\n            canvas[mask] = instance_id\n            instance_id += 1\n\n        if canvas.max() > 0:\n            instance_ids = np.unique(canvas)[1:]\n            for inst_id in instance_ids:\n                binary_mask = (canvas == inst_id).astype(np.uint8)\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': ''})\n\n    # === 輸出 CSV ===\n    submission_df = pd.DataFrame(submission_data, columns=['id', 'predicted'])\n    all_test_ids = [f.stem for f in test_files]\n    missing_ids = set(all_test_ids) - set(submission_df['id'].unique())\n    for img_id in missing_ids:\n        submission_df = pd.concat(\n            [submission_df, pd.DataFrame([{'id': img_id, 'predicted': ''}])],\n            ignore_index=True\n        )\n    submission_df = submission_df.sort_values(by='id').reset_index(drop=True)\n    submission_df.to_csv(output_csv, index=False)\n    print(f\"✅ 輸出完成：{output_csv}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:37.684723Z","iopub.execute_input":"2025-06-02T14:12:37.684890Z","iopub.status.idle":"2025-06-02T14:12:37.703065Z","shell.execute_reply.started":"2025-06-02T14:12:37.684874Z","shell.execute_reply":"2025-06-02T14:12:37.702515Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_dir = Path('/kaggle/input/sartorius-cell-instance-segmentation/test')\nensemble_and_generate_submission(test_dir=test_dir)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:12:37.704037Z","iopub.execute_input":"2025-06-02T14:12:37.704356Z","iopub.status.idle":"2025-06-02T14:13:35.068019Z","shell.execute_reply.started":"2025-06-02T14:12:37.704325Z","shell.execute_reply":"2025-06-02T14:13:35.067277Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from collections import defaultdict\nfrom tqdm import tqdm\n\nsubmission_df = pd.read_csv('submission.csv')\n\n# === 建立 rle_dict 從 submission_df 讀入 ===\nrle_dict = defaultdict(list)\nfor _, row in submission_df.iterrows():\n    rle_dict[row['id']].append(row['predicted'])\n\n# === 檢查是否有 pixel 重疊 ===\noverlap_count = 0\nfor img_id, rles in tqdm(rle_dict.items(), desc=\"🔍 檢查像素重疊\"):\n    mask_sum = np.zeros((520, 704), dtype=np.uint8)\n    for rle_text in rles:\n        if isinstance(rle_text, float) and np.isnan(rle_text):\n            continue\n        if rle_text.strip() == '':\n            continue\n        mask = rle_decode(rle_text, (520, 704))\n        mask_sum += mask\n    if (mask_sum > 1).any():\n        print(f\"⚠️ 圖像 {img_id} 有重疊的 pixel！\")\n        overlap_count += 1\n\nif overlap_count == 0:\n    print(\"✅ 所有圖像都沒有重疊 pixel\")\nelse:\n    print(f\"⚠️ 共 {overlap_count} 張圖像有重疊問題，建議檢查合併策略\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:13:35.068966Z","iopub.execute_input":"2025-06-02T14:13:35.069214Z","iopub.status.idle":"2025-06-02T14:13:35.117968Z","shell.execute_reply.started":"2025-06-02T14:13:35.069195Z","shell.execute_reply":"2025-06-02T14:13:35.117232Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ✅ 檢查\nsubmission_df = pd.read_csv('submission.csv')\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-02T14:13:35.118739Z","iopub.execute_input":"2025-06-02T14:13:35.118969Z","iopub.status.idle":"2025-06-02T14:13:35.132859Z","shell.execute_reply.started":"2025-06-02T14:13:35.118946Z","shell.execute_reply":"2025-06-02T14:13:35.132111Z"}},"outputs":[],"execution_count":null},{"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-02T14:13:35.133630Z","iopub.execute_input":"2025-06-02T14:13:35.133914Z","iopub.status.idle":"2025-06-02T14:13:35.139112Z","shell.execute_reply.started":"2025-06-02T14:13:35.133897Z","shell.execute_reply":"2025-06-02T14:13:35.138605Z"}},"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-02T14:13:35.139738Z","iopub.execute_input":"2025-06-02T14:13:35.139922Z","iopub.status.idle":"2025-06-02T14:13:37.390248Z","shell.execute_reply.started":"2025-06-02T14:13:35.139908Z","shell.execute_reply":"2025-06-02T14:13:37.389442Z"}},"outputs":[],"execution_count":null}]}