{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":30201,"databundleVersionId":2750748,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":11861404,"sourceType":"datasetVersion","datasetId":7453452},{"sourceId":12023131,"sourceType":"datasetVersion","datasetId":7564337}],"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/cellpose-linux-whl-v2/wheels --no-deps cellpose fastremap fill_voids roifile","metadata":{"_uuid":"7ad6ad9b-41bb-40db-a006-66a3d5417d84","_cell_guid":"b96a2235-70ef-4e2f-96ac-017543dde9b6","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from cellpose import models, io\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nimport cv2\n\n# -------------------------------------------------------------------\n# 【改動起點】：從「單一 model」→「多個 checkpoint Ensemble」\n# -------------------------------------------------------------------\n\n# 1) 把你要做 Ensemble 的 checkpoint 路徑都放進列表裡\nensemble_weights = [\n    #'/kaggle/input/patch4_base_bs2/pytorch/default/1/patch_epoch100_bs2_epoch_0031',\n    # '/kaggle/input/cosbest/pytorch/default/1/patch_epoch100_bs2__epoch_0031',\n    #'/kaggle/input/full-res/fullres_epoch_0035',\n    # \"/kaggle/input/cellpose-fold-weight/fold0_epoch_0032_mAP_0.3129.pth\",\n    \"/kaggle/input/cellpose-fold-weight/fold1_epoch_0038_mAP_0.2929.pth\",\n    # \"/kaggle/input/cellpose-fold-weight/fold2_epoch_0058_mAP_0.3104.pth\",\n    \"/kaggle/input/cellpose-fold-weight/fold3_epoch_0067_mAP_0.3076.pth\",\n    \"/kaggle/input/cellpose-fold-weight/fold4_epoch_0047_mAP_0.2945.pth\",\n    # 如果還有更多，繼續在這裡加上路徑\n]\n\n# 2) 針對每個 checkpoint path 建立一個 CellposeModel 實例，放到 models_list\nmodels_list = [\n    models.CellposeModel(\n        gpu=True,\n        pretrained_model=weight_path\n    )\n    for weight_path in ensemble_weights\n]\n\n# 3) （選擇性）確認載入完畢\nprint(f\"Loaded {len(models_list)} models for ensemble.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def rle_encode(img):\n    '''\n    img: numpy array, 1 - mask, 0 - background\n    Returns run length as string formated\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)\n\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","metadata":{"trusted":true},"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # Cell 4（整個 cell 內容換成下面這段 Ensemble 版）\n# import numpy as np\n# import cv2\n\n# def tta_ensemble_predict(models_list, image):\n#     \"\"\"\n#     Ensemble TTA：針對多個 CellposeModel (models_list) + 兩種增強（原圖、水平翻轉），\n#     先對同一模型的兩個增強平均，再對所有模型平均。\n#     參數：\n#       - models_list: list of CellposeModel instances\n#       - image: (H, W, 3) numpy array\n#     回傳：\n#       averaged_prob_map_ensemble: (H, W) float32\n#       averaged_dp_map_ensemble:   (2, H, W) float32\n#     \"\"\"\n#     H, W = image.shape[:2]\n\n#     # 定義兩種增強：原圖、水平翻轉，以及對應的逆變換\n#     aug_images = [\n#         image,\n#         np.flip(image, axis=1)\n#     ]\n#     inverse_ops_prob = [\n#         lambda x: x,\n#         lambda x: np.flip(x, axis=1),\n#     ]\n#     inverse_ops_dp = [\n#         lambda dp: dp,\n#         lambda dp: np.stack([\n#             np.flip(dp[0], axis=1),\n#             -np.flip(dp[1], axis=1)\n#         ], axis=0),\n#     ]\n\n#     # all_prob_maps[i][j] 表示第 i 個模型 第 j 種增強 的 prob_map\n#     # all_dp_maps[i][j]   表示第 i 個模型 第 j 種增強 的 dp_map\n#     n_models = len(models_list)\n#     all_prob_maps = []\n#     all_dp_maps   = []\n\n#     for m in models_list:\n#         prob_list_per_model = []\n#         dp_list_per_model   = []\n\n#         for aug_idx, aug_img in enumerate(aug_images):\n#             # 不做完整的 mask 解碼，只拿到 eval_flows\n#             _, eval_flows, _ = m.eval(aug_img, compute_masks=False)\n#             dp_raw   = eval_flows[1].astype(np.float32)  # (2, h', w')\n#             prob_raw = eval_flows[2].astype(np.float32)  # (h', w')\n\n#             # 先做逆變換（flip 回來、resize 回 (H,W)）\n#             prob_aug = inverse_ops_prob[aug_idx](prob_raw)\n#             if prob_aug.shape[:2] != (H, W):\n#                 prob_aug = cv2.resize(prob_aug, (W, H), interpolation=cv2.INTER_LINEAR)\n\n#             dp_aug = inverse_ops_dp[aug_idx](dp_raw)\n#             if dp_aug.shape[1:] != (H, W):\n#                 dp_x = cv2.resize(dp_aug[0], (W, H), interpolation=cv2.INTER_LINEAR)\n#                 dp_y = cv2.resize(dp_aug[1], (W, H), interpolation=cv2.INTER_LINEAR)\n#                 dp_aug = np.stack([dp_x, dp_y], axis=0)\n\n#             prob_list_per_model.append(prob_aug)  # shape=(H,W)\n#             dp_list_per_model.append(dp_aug)      # shape=(2,H,W)\n\n#         # 對同一模型的「原圖＋水平翻轉」先做平均\n#         prob_avg_i = np.mean(np.stack(prob_list_per_model, axis=0), axis=0)  # (H,W)\n#         dp_avg_i   = np.mean(np.stack(dp_list_per_model, axis=0), axis=0)    # (2,H,W)\n\n#         all_prob_maps.append(prob_avg_i)  # (H,W)\n#         all_dp_maps.append(dp_avg_i)      # (2,H,W)\n\n#     # 再對所有模型的結果做平均\n#     averaged_prob_map_ensemble = np.mean(np.stack(all_prob_maps, axis=0), axis=0)  # (H,W)\n#     averaged_dp_map_ensemble   = np.mean(np.stack(all_dp_maps, axis=0), axis=0)    # (2,H,W)\n\n#     return averaged_prob_map_ensemble, averaged_dp_map_ensemble\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"final_ids, final_masks = [], []","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pycocotools.mask\nimport torch\nimport numpy as np\nimport cv2\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    Weighted NMW：\n      1. 計算所有 masks 之間的 IoU → ious (N×N)\n      2. 把 IoU > iou_thr 的那些 masks 視為同 cluster\n      3. cluster 內每張 mask 的分數乘上 score_coef，再 normalize → w_k\n      4. cluster 內所有 mask 做 pixel‐wise weighted sum → soft_map\n      5. soft_map >= 0.5 threshold → fused mask；若全部 < 0.5，則取 cluster 中分數最高那張 mask\n    Args:\n        masks (np.ndarray): shape=(N, H, W)，二值 mask list\n        boxes (np.ndarray): shape=(N, 6)，[x0,y0,x1,y1,area, dummy_score]\n        scores (np.ndarray): shape=(N,) 對應每張 mask 的「輔助分數」\n        iou_thr (float): cluster 分群 IoU 閾值\n        score_coef (float): 分數弱化係數 (< 1)\n    Returns:\n        fused_masks (np.ndarray): shape=(M, H, W)，融合後二值 masks\n        fused_boxes (np.ndarray): shape=(M, 6)，融合後 bounding boxes\n    \"\"\"\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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport cv2\n\ndef tta_predict_probability_and_flows(model, image, image_id):\n    \"\"\"\n    使用 TTA（原圖 + 水平翻轉）來計算該模型對單張影像的 averaged_prob_map 與 averaged_dp_map。\n    參數：\n      - model: CellposeModel instance\n      - image: (H, W, 3) numpy array\n      - image_id: string (可忽略)\n    回傳：\n      - averaged_prob_map: (H, W) numpy float32\n      - averaged_dp_map:   (2, H, W) numpy float32\n    \"\"\"\n    H, W = image.shape[:2]\n\n    # 我們只做「原圖」和「水平翻轉」兩種增強\n    aug_images = [\n        image,\n        np.flip(image, axis=1)\n    ]\n    # 對應的逆變換：水平翻轉後要再翻回來\n    inverse_ops_prob = [\n        lambda x: x,\n        lambda x: np.flip(x, axis=1),\n    ]\n    inverse_ops_dp = [\n        lambda dp: dp,\n        lambda dp: np.stack([\n            np.flip(dp[0], axis=1),\n            -np.flip(dp[1], axis=1)\n        ], axis=0),\n    ]\n\n    all_prob_maps = []\n    all_dp_maps   = []\n\n    for aug_idx, aug_img in enumerate(aug_images):\n        # 只取 eval_flows，不做完整的 mask 解碼\n        _, eval_flows, _ = model.eval(aug_img, compute_masks=False)\n        pred_dp_map_raw = eval_flows[1].astype(np.float32)  # shape=(2, h', w')\n        pred_prob_map_raw = eval_flows[2].astype(np.float32)  # shape=(h', w')\n\n        # 先做逆變換（flip 回去）\n        prob_aug = inverse_ops_prob[aug_idx](pred_prob_map_raw)\n        dp_aug   = inverse_ops_dp[aug_idx](pred_dp_map_raw)\n\n        # 若輸出尺寸跟原圖不同，就 resize 回 (H, W)\n        if prob_aug.shape[:2] != (H, W):\n            prob_aug = cv2.resize(prob_aug, (W, H), interpolation=cv2.INTER_LINEAR)\n\n        if dp_aug.shape[1:] != (H, W):\n            dp_x = cv2.resize(dp_aug[0], (W, H), interpolation=cv2.INTER_LINEAR)\n            dp_y = cv2.resize(dp_aug[1], (W, H), interpolation=cv2.INTER_LINEAR)\n            dp_aug = np.stack([dp_x, dp_y], axis=0)\n\n        all_prob_maps.append(prob_aug)\n        all_dp_maps.append(dp_aug)\n\n    # 在兩張增強結果上做平均\n    averaged_prob_map = np.mean(np.stack(all_prob_maps, axis=0), axis=0)  # shape=(H, W)\n    averaged_dp_map   = np.mean(np.stack(all_dp_maps, axis=0), axis=0)    # shape=(2, H, W)\n\n    return averaged_prob_map, averaged_dp_map\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================\n# Cell 7：推論 + Ensemble (多模型 TTA) + NMW 後處理\n# =============================\nfrom collections import defaultdict\nfrom tqdm import tqdm\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nimport skimage.io as io\nfrom cellpose import dynamics, models, core, io as cpio, metrics\nimport cv2\n\n# 以下幾個參數需與驗證階段保持一致\ndecode_flow_th = 0.4    # Cellpose 解流向時使用的 threshold\niou_thr_nmw    = 0.45   # NMW 聚類時的 IoU 閾值\nscore_coef     = 0.8    # NMW 分數弱化係數 (<1)\nmin_size       = 75     # 最終篩除面積 < 75 px² 的 mask\ncorrupt        = True   # 是否對 astrocyte 做 convex‐hull 補洞\ncell_type      = 0      # 1 表示 astrocyte 才做補洞；0 表示不做\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 = []\nfor img_path in tqdm(test_files, desc=\"🚀 推論中\"):\n    img_id = img_path.stem\n    img = io.imread(str(img_path))  # 讀取影像，格式為 (H, W, 3)\n    H, W = img.shape[:2]\n\n    # ===== 1. 逐模型做 TTA → dynamics 解 mask → 拆出 m_bin, bxs, 並計算「平均機率」做為 score =====\n    all_masks, all_boxes, all_scores = [], [], []\n    for mdl in models_list:\n        # 1.1) 本模型做 TTA (原圖 + 水平翻轉)，取得 avg_prob_map, avg_dp_map\n        avg_prob_map, avg_dp_map = tta_predict_probability_and_flows(mdl, img, \"\")\n\n        \n        # 1.2) 用 dynamics 解出稠密的 instance map\n        combined_mask = dynamics.resize_and_compute_masks(\n            avg_dp_map,\n            avg_prob_map,\n            cellprob_threshold=0.0,\n            flow_threshold=decode_flow_th,\n            min_size=20,    # 初步篩除極小物件\n            resize=(H, W),\n            device=mdl.device\n        )\n        # 如果此模型完全沒偵測到任何實例，就跳過\n        if combined_mask.max() == 0:\n            continue\n\n        # 1.3) 把 dense instance map 拆成多張二值 mask 與對應的 boxes\n        m_bin, bxs = instmap_to_masks_boxes(combined_mask)\n        if m_bin is None:\n            continue\n\n        # 1.4) 計算每個實例在 avg_prob_map 上的平均機率，做為該實例的 score\n        n_inst = m_bin.shape[0]\n        inst_scores = []\n        for k in range(n_inst):\n            mask_k = m_bin[k].astype(bool)\n            if mask_k.sum() == 0:\n                inst_scores.append(0.0)\n            else:\n                inst_scores.append(float(avg_prob_map[mask_k].mean()))\n\n        # 1.5) 收集本模型拆出的所有 instance (mask, box, score)\n        all_masks.append(m_bin)                   # shape = (k_i, H, W)\n        all_boxes.append(bxs)                     # shape = (k_i, 6)\n        all_scores.append(np.array(inst_scores))  # shape = (k_i,)\n\n    # ===== 如果所有模型都沒偵測到任何實例，則輸出空白 =====\n    if not all_masks:\n        submission_data.append({'id': img_id, 'predicted': ''})\n        continue\n\n    # ===== 2. 合併所有模型的 masks / boxes / scores =====\n    masks_concat  = np.concatenate(all_masks, axis=0)   # shape=(N_total, H, W)\n    boxes_concat  = np.concatenate(all_boxes, axis=0)   # shape=(N_total, 6)\n    scores_concat = np.concatenate(all_scores, axis=0)  # shape=(N_total,)\n\n    # ===== 3. 一次性呼叫 Weighted Mask Fusion (NMW) 進行加權融合 =====\n    fused_masks, fused_boxes = weighted_mask_fusion_nmw(\n        masks_concat, boxes_concat, scores_concat,\n        iou_thr=iou_thr_nmw,\n        score_coef=score_coef\n    )\n    # fused_masks shape=(M, H, W)\n\n    # ===== 4. 小面積過濾 =====\n    if fused_masks.shape[0] > 0:\n        areas = fused_masks.sum(axis=(1, 2))\n        keep = np.where(areas > min_size)[0]\n        fused_masks = fused_masks[keep]\n        fused_boxes = fused_boxes[keep]\n\n    # ===== 5. astrocyte convex‐hull 補洞（若啟用） =====\n    if corrupt and (cell_type == 1) and (fused_masks.shape[0] > 0):\n        corrected = []\n        for m in fused_masks:\n            conts, _ = cv2.findContours(\n                m.astype(np.uint8),\n                cv2.RETR_EXTERNAL,\n                cv2.CHAIN_APPROX_SIMPLE\n            )\n            cvx_mask = np.zeros_like(m, dtype=np.uint8)\n            for cnt in conts:\n                hull = cv2.convexHull(cnt)\n                cvx_mask = cv2.fillConvexPoly(cvx_mask, hull, 1)\n            corrected.append(cvx_mask)\n        fused_masks = np.stack(corrected, axis=0)\n\n    # ===== 6. 最後一次 Greedy Overlap Removal =====\n    if fused_masks.shape[0] > 0:\n        fused_masks = remove_overlap_naive(fused_masks)\n\n    # ===== 7. 重建最終稠密 instance map，並做 RLE 編碼輸出 =====\n    canvas = np.zeros((H, W), dtype=np.int32)\n    for m in fused_masks:\n        canvas[m.astype(bool)] = canvas.max() + 1\n\n    if canvas.max() > 0:\n        instance_ids = np.unique(canvas)\n        instance_ids = instance_ids[instance_ids != 0]\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# ===== 最後整理 submission_df、檢查重複與重疊，並輸出 submission.csv =====\nsubmission_df = pd.DataFrame(submission_data, columns=['id', 'predicted'])\nall_test_ids = [f.stem for f in test_files]\nmissing_ids = set(all_test_ids) - set(submission_df['id'].unique())\nfor img_id in missing_ids:\n    submission_df = pd.concat(\n        [submission_df, pd.DataFrame([{'id': img_id, 'predicted': ''}])],\n        ignore_index=True\n    )\nsubmission_df = submission_df.sort_values(by='id').reset_index(drop=True)\n\nduplicate_ids = submission_df.duplicated(subset=['id'], keep=False)\nif duplicate_ids.any():\n    print(\"⚠️ 存在重複 id，請檢查合併邏輯！\")\n\nrle_dict = defaultdict(list)\nfor img_id, rle_text in zip(submission_df['id'], submission_df['predicted']):\n    if isinstance(rle_text, str) and rle_text.strip() != '':\n        rle_dict[img_id].append(rle_text)\n\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        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} 張圖像有重疊問題，建議檢查合併策略\")\n\nsubmission_df.to_csv('submission.csv', index=False)\n","metadata":{"trusted":true},"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ✅ 終端輸出資訊\nprint(submission_df.head(20))\nprint(\"欄位名稱:\", submission_df.columns)\nprint(\"是否有重複 id:\", submission_df['id'].duplicated().any())\nprint(\"是否有 null:\", submission_df.isnull().sum())\nprint(\"是否有空字串以外的空值:\", (submission_df['predicted'].astype(str).str.strip() == '').sum())\nprint(\"id 總數:\", submission_df['id'].nunique(), \"submission 行數:\", len(submission_df))\nprint(\"predicted 欄型別:\", submission_df['predicted'].apply(type).value_counts())\nprint(\"✅ Submission file 'submission.csv' created successfully!\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}