{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"sourceType":"competition"},{"sourceId":14354036,"sourceType":"datasetVersion","datasetId":9158324},{"sourceId":14425712,"sourceType":"datasetVersion","datasetId":9214051},{"sourceId":270092713,"sourceType":"kernelVersion"}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install --no-index /kaggle/input/lightglue-wheel/lightglue-0.0-py3-none-any.whl","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!mkdir -p /root/.cache/torch/hub/checkpoints\n!cp /kaggle/input/lightglue-wheel/superpoint_v1.pth /root/.cache/torch/hub/checkpoints/superpoint_v1.pth\n!cp /kaggle/input/lightglue-wheel/superpoint_lightglue_v0-1_arxiv.pth /root/.cache/torch/hub/checkpoints/superpoint_lightglue_v0-1_arxiv.pth","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport metric\nimport os\nfrom matplotlib import pyplot as plt\nfrom pathlib import Path\nimport cv2\nfrom lightglue import LightGlue, SuperPoint\nfrom lightglue.utils import load_image, rbd\nfrom lightglue import viz2d\nimport torch\n\nTILE_HIST_VAR_THRESHOLD=0.7\nAFFINE_REPROJ_THRESH=1.0\nLIGHTGLUE_MATCH_SCORE_THRESHOLD=0.9","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T00:00:30.755906Z","iopub.execute_input":"2025-12-30T00:00:30.75628Z","iopub.status.idle":"2025-12-30T00:00:41.469702Z","shell.execute_reply.started":"2025-12-30T00:00:30.756252Z","shell.execute_reply":"2025-12-30T00:00:41.469107Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def hist_total_variation(gray_u8):\n    # gray_u8: uint8 image\n    hist = np.bincount(gray_u8.ravel(), minlength=256).astype(np.float64)\n    p = hist / hist.sum()\n    tv = np.abs(np.diff(p)).sum()\n    return tv\n\ndef filter_out_intersecing_boxes(boxes):\n    indices_to_remove = []\n    for i in range(len(boxes)-1):\n        if i in indices_to_remove:\n            continue\n        for j in range(i+1, len(boxes)):\n            if j in indices_to_remove:\n                continue\n            x0, y0, w0, h0 = boxes[i]\n            x1, y1, w1, h1 = boxes[j]\n\n            #check whether intersect\n            if not (x0+w0 < x1\n                or x0 > x1+w1\n                or y0+h0 < y1\n                or y0 > y1+h1):\n                # flag smaller tile for removal\n                if w0*h0 < w1*h1:\n                    indices_to_remove.append(i)\n                else:\n                    indices_to_remove.append(j)\n    return [b for (i, b) in enumerate(boxes) if i not in indices_to_remove]\n\ndef max_filter_1d(x, k):\n    \"\"\"Simple 1D max filter with window k (k odd).\"\"\"\n    pad = k // 2\n    xp = np.pad(x, (pad, pad), mode=\"edge\")\n    return np.maximum.reduce([xp[i:i+len(x)] for i in range(k)])\n    \ndef merge_runs(mask):\n    \"\"\"Return center indices of consecutive True runs.\"\"\"\n    idx = np.flatnonzero(mask)\n    if len(idx) == 0:\n        return []\n    runs = []\n    start = idx[0]\n    prev = idx[0]\n    for i in idx[1:]:\n        if i == prev + 1:\n            prev = i\n        else:\n            runs.append((start, prev))\n            start = prev = i\n    runs.append((start, prev))\n    return [ (a + b) // 2 for a, b in runs ]\n    \ndef detect_gridlines(gray, canny1=50, canny2=150, coverage_thr=0.5, thick_k=5):\n    \"\"\"\n    coverage_thr: fraction of width that must be edge pixels (e.g. 0.5 = 50%)\n    thick_k: vertical max-filter window to collapse thick edges (odd number, e.g. 3/5/7)\n    \"\"\"\n    edges = cv2.Canny(gray, canny1, canny2)          # 0 or 255\n    E = (edges > 0).astype(np.uint8)                # 0/1\n    \n    H, W = E.shape\n    row_cov = E.sum(axis=1) / float(W)              # fraction per row\n    col_cov = E.sum(axis=0) / float(H)\n    \n    # Collapse thickness: if any row within +/- (thick_k//2) is high, treat as high\n    row_cov_pooled = max_filter_1d(row_cov, thick_k)\n    col_cov_pooled = max_filter_1d(col_cov, thick_k)\n    \n    rows_is_line = row_cov_pooled >= coverage_thr\n    y_lines = merge_runs(rows_is_line)              # one y per gridline\n    \n    cols_is_line = col_cov_pooled >= coverage_thr\n    x_lines = merge_runs(cols_is_line)\n    \n    return x_lines, y_lines, edges, row_cov, row_cov_pooled, rows_is_line\n    \ndef split_if_table(img_gray):\n    x_lines, y_lines, _, _, _, _ = detect_gridlines(\n        img_gray, coverage_thr=0.5, thick_k=5\n    )\n    \n    H, W = img_gray.shape\n    \n    x_lines_filtered = []\n    x_prev = 0\n    for x in x_lines:\n        if x > x_prev+50 and x < W-50:\n            x_lines_filtered.append(x)\n            x_prev = x\n            \n    y_lines_filtered = []\n    y_prev = 0\n    for y in y_lines:\n        if y > y_prev+50 and y < H-50:\n            y_lines_filtered.append(y)\n            y_prev = y\n            \n    x_lines_filtered.insert(0, 0)\n    x_lines_filtered.append(W)\n    y_lines_filtered.insert(0, 0)\n    y_lines_filtered.append(H)\n    \n    boxes = []\n    for x_ind in range(len(x_lines_filtered)-1):\n        for y_ind in range(len(y_lines_filtered)-1):\n            boxes.append(\n                (\n                    x_lines_filtered[x_ind],\n                    y_lines_filtered[y_ind],\n                    x_lines_filtered[x_ind+1] - x_lines_filtered[x_ind],\n                    y_lines_filtered[y_ind+1] - y_lines_filtered[y_ind]\n                )\n            )\n    \n    return boxes\n\ndef detect_tiles(img_path):\n    img = cv2.imread(img_path)\n    assert img is not None, f\"Can't read {img_path}\"\n\n    H, W = img.shape[:2]\n    \n    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n    \n    # Foreground = pixels darker than T (tune T)\n    T = 245   # try 235, 240, 245, 250 depending on scan/compression\n    fg = (gray < T).astype(np.uint8) * 255\n    \n    # Clean tiny specks and fill tiny gaps (DO NOT use huge kernels here)\n    fg = cv2.morphologyEx(fg, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3)), iterations=1)\n    fg = cv2.morphologyEx(fg, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3)), iterations=1)\n\n    cnts, _ = cv2.findContours(fg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n\n    boxes = []\n    min_area = 0.005 * (W * H)   # start: 0.5% of page; tune as needed\n    \n    for c in cnts:\n        x, y, w, h = cv2.boundingRect(c)\n        area = w * h\n        if area < min_area:\n            continue\n        if w < 50 or h < 50:\n            continue\n        if hist_total_variation(img[y:(y+h), x:(x+w)].mean(axis=2).astype('int64')) > TILE_HIST_VAR_THRESHOLD:\n            continue\n        boxes.append((x, y, w, h))\n\n    boxes = filter_out_intersecing_boxes(boxes)\n\n    #boxes = [(x+x_, y+y_, w_, h_) for (x, y, w, h) in boxes for (x_, y_, w_, h_) in split_if_table(gray[y:(y+h), x:(x+w)])]\n\n    return boxes","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T00:00:41.470798Z","iopub.execute_input":"2025-12-30T00:00:41.471264Z","iopub.status.idle":"2025-12-30T00:00:41.478542Z","shell.execute_reply.started":"2025-12-30T00:00:41.471237Z","shell.execute_reply":"2025-12-30T00:00:41.477827Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def ransac_affine(m_kpts0, m_kpts1, reproj_thresh, max_iters=5000, conf=0.999):\n    # LightGlue keypoints are (x,y). Convert to numpy float32.\n    P = m_kpts0.detach().cpu().numpy().astype(np.float32)\n    Q = m_kpts1.detach().cpu().numpy().astype(np.float32)\n\n    if len(P) < 8:\n        return None, None, None\n\n    M, inl = cv2.estimateAffine2D(\n        P, Q,\n        method=cv2.RANSAC,\n        ransacReprojThreshold=reproj_thresh,\n        maxIters=max_iters,\n        confidence=conf\n    )\n    if M is None or inl is None:\n        return None, None, None\n\n    inlier = inl.ravel().astype(bool)\n\n    # residuals (for quality checks / debugging)\n    P_h = np.hstack([P, np.ones((len(P), 1), np.float32)])\n    Q_pred = (M @ P_h.T).T\n    resid = np.linalg.norm(Q - Q_pred, axis=1)\n\n    return M, inlier, resid\n\ndef min_area_box(points_xy):\n    rect = cv2.minAreaRect(points_xy.astype(np.float32))\n    box = cv2.boxPoints(rect)  # (4,2)\n    return box","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T00:00:41.47945Z","iopub.execute_input":"2025-12-30T00:00:41.479677Z","iopub.status.idle":"2025-12-30T00:00:41.494695Z","shell.execute_reply.started":"2025-12-30T00:00:41.479646Z","shell.execute_reply":"2025-12-30T00:00:41.494102Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nextractor = SuperPoint(max_num_keypoints=2048).eval().to(device)\nmatcher = LightGlue(features='superpoint', depth_confidence=-1).eval().to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T00:00:41.495806Z","iopub.execute_input":"2025-12-30T00:00:41.496047Z","iopub.status.idle":"2025-12-30T00:00:43.15591Z","shell.execute_reply.started":"2025-12-30T00:00:41.496005Z","shell.execute_reply":"2025-12-30T00:00:43.155294Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def detect_matches(#image0, image1,\n                   feats0, feats1,\n                   reproj_thresh,\n                   min_inliers=20,\n                   min_inlier_ratio=0.2,\n                   max_med_resid=4.0,\n                   draw=False):\n    #feats0 = extractor.extract(image0.to(device))\n    #feats1 = extractor.extract(image1.to(device))\n    matches01 = matcher({\"image0\": feats0, \"image1\": feats1})\n\n    feats0, feats1, matches01 = [rbd(x) for x in [feats0, feats1, matches01]]  # remove batch\n\n    kpts0, kpts1 = feats0[\"keypoints\"], feats1[\"keypoints\"]\n    matches = matches01[\"matches\"]  # (K,2) torch tensor\n\n    scores  = matches01[\"scores\"]\n    matches = matches[scores >= LIGHTGLUE_MATCH_SCORE_THRESHOLD]\n\n    if matches.numel() == 0 or len(matches) < 3:\n        if draw:\n            viz2d.plot_images([image0, image1])\n            viz2d.add_text(0, f\"No matches after score filter (thr={LIGHTGLUE_MATCH_SCORE_THRESHOLD})\", fs=18)\n        return None\n        \n    # matched keypoints\n    m_kpts0 = kpts0[matches[:, 0]]\n    m_kpts1 = kpts1[matches[:, 1]]\n\n    # ---- RANSAC consistency check ----\n    M, inlier_mask, resid = ransac_affine(m_kpts0, m_kpts1, reproj_thresh=reproj_thresh)\n\n    if M is None:\n        if draw:\n            viz2d.plot_images([image0, image1])\n            viz2d.add_text(0, \"No affine model (too few / degenerate matches)\", fs=18)\n        return None\n\n    ninl = int(inlier_mask.sum())\n    K = len(inlier_mask)\n    inlier_ratio = ninl / max(K, 1)\n    med_resid = float(np.median(resid[inlier_mask])) if ninl > 0 else float(\"inf\")\n\n    good = (ninl >= min_inliers) and (inlier_ratio >= min_inlier_ratio) and (med_resid <= max_med_resid)\n\n    if not good:\n        if draw:\n            viz2d.plot_images([image0, image1])\n            viz2d.add_text(\n                0,\n                f\"No consistent region: inliers={ninl}/{K} ({inlier_ratio:.2f}), med_err={med_resid:.2f}px\",\n                fs=18\n            )\n        return None\n\n    # keep only inliers for downstream region estimation / plotting\n    inlier_mask_t = torch.from_numpy(inlier_mask).to(matches.device)\n    inlier_matches = matches[inlier_mask_t]\n    inlier_kpts0 = m_kpts0[inlier_mask_t]\n    inlier_kpts1 = m_kpts1[inlier_mask_t]\n\n    # ---- bounding rectangles (rotated) in both images ----\n    P_in = inlier_kpts0.detach().cpu().numpy().astype(np.float32)\n    Q_in = inlier_kpts1.detach().cpu().numpy().astype(np.float32)\n    box0 = min_area_box(P_in)\n    box1 = min_area_box(Q_in)\n\n    if draw:\n        viz2d.plot_images([image0, image1])\n        viz2d.plot_matches(inlier_kpts0, inlier_kpts1, color=\"lime\", lw=0.4)\n        viz2d.add_text(\n            0,\n            f\"inliers={ninl}/{K} ({inlier_ratio:.2f}), med_err={med_resid:.2f}px, stop={matches01['stop']}\",\n            fs=18\n        )\n        # Optional: also show all keypoints, prunes, etc. (as you did)\n        # viz2d.plot_keypoints([...])\n\n        # If viz2d doesn't support polygons, you can overlay boxes via matplotlib later.\n        # For now we just return them.\n\n    return {\n        \"M\": M,\n        \"inlier_matches\": inlier_matches,\n        \"inlier_kpts0\": inlier_kpts0,\n        \"inlier_kpts1\": inlier_kpts1,\n        \"box0\": box0,\n        \"box1\": box1,\n        \"stats\": {\"inliers\": ninl, \"total\": K, \"ratio\": inlier_ratio, \"median_residual\": med_resid}\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T00:00:44.121751Z","iopub.execute_input":"2025-12-30T00:00:44.122103Z","iopub.status.idle":"2025-12-30T00:00:44.132643Z","shell.execute_reply.started":"2025-12-30T00:00:44.122069Z","shell.execute_reply":"2025-12-30T00:00:44.132097Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"bin_thresh = 0.3\npoly_thresh = 0.5\ntextDetectorDB50 = cv2.dnn_TextDetectionModel_DB(\"/kaggle/input/recod-ai-sifd-text-detection-db50/DB_TD500_resnet50.onnx\")\ntextDetectorDB50.setBinaryThreshold(bin_thresh).setPolygonThreshold(poly_thresh)\n\ndef sanitize_imread_bgr(img: np.ndarray) -> np.ndarray:\n    if img.ndim == 2:\n        img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)\n    elif img.ndim == 3:\n        if img.shape[2] == 1:\n            img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)\n        elif img.shape[2] == 4:\n            # Only possible if you used IMREAD_UNCHANGED somewhere; otherwise imread default is 3-ch.\n            img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)\n\n    return img\n\ndef db50_text_location(orig_img):\n    orig_img = sanitize_imread_bgr(orig_img)\n    mean = orig_img.mean(axis=(0, 1))\n\n    orig_shape = orig_img.shape[:2]\n    img = cv2.resize(orig_img, (480, 480))\n    shape = img.shape[:2]\n    \n    textDetectorDB50.setInputParams(1.0/255, shape, mean, True)\n    \n    inpaint_mask_db50 = np.zeros(img.shape[:2], dtype=np.uint8)\n    \n    boxesDB50, _ = textDetectorDB50.detect(img)\n\n    if boxesDB50 is None:\n        return cv2.resize(inpaint_mask_db50, orig_shape[::-1], interpolation=cv2.INTER_NEAREST)\n    \n    for box in boxesDB50:\n        cv2.fillPoly(inpaint_mask_db50, [np.array(box, np.int32)], 255)\n\n    mask = cv2.resize(inpaint_mask_db50, orig_shape[::-1], interpolation=cv2.INTER_NEAREST)\n    \n    return mask\n\ndef filter_keypoints_by_mask(_features, _text_mask):\n    try:\n        keypoints = _features['keypoints'][0]\n        x = keypoints[:, 0].long().cpu()\n        y = keypoints[:, 1].long().cpu()\n        valid = ~_text_mask[y, x].astype(bool)\n    \n        res = _features.copy()\n        \n        res['keypoints'] = res['keypoints'][:, valid, :]\n        res['keypoint_scores'] = res['keypoint_scores'][:, valid]\n        res['descriptors'] = res['descriptors'][:, valid, :]\n    except Exception:\n        return _features\n\n    return res","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def draw_box(ax, box, lw=2):\n    pts = np.vstack([box, box[0]])\n    ax.plot(pts[:,0], pts[:,1], linewidth=lw)\n\ndef show_matching_boxes(image0, image1, res):\n    fig, axes = plt.subplots(1,2, figsize=(12,6))\n    axes[0].imshow(image0.permute(1,2,0).cpu())\n    axes[1].imshow(image1.permute(1,2,0).cpu())\n    draw_box(axes[0], res[\"box0\"])\n    draw_box(axes[1], res[\"box1\"])\n    for a in axes: a.axis(\"off\")\n    plt.show()\n\ndef draw_boxes(boxes, img_path):\n    img = cv2.imread(img_path)\n    dbg = img.copy()\n    \n    for (x, y, w, h) in boxes:\n        cv2.rectangle(dbg, (x, y), (x+w, y+h), (0, 255, 0), 2)\n    plt.figure(figsize=(8,8))\n    plt.imshow(dbg)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T00:00:47.10509Z","iopub.execute_input":"2025-12-30T00:00:47.10577Z","iopub.status.idle":"2025-12-30T00:00:47.112022Z","shell.execute_reply.started":"2025-12-30T00:00:47.105739Z","shell.execute_reply":"2025-12-30T00:00:47.111312Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def masks_intersect(a, b):\n    inter = np.logical_and(a, b).sum(dtype=np.int64)\n    area_a = a.sum(dtype=np.int64)\n    area_b = b.sum(dtype=np.int64)\n\n    union = area_a + area_b - inter\n    iou = inter / union if union > 0 else 0.0\n\n    return iou > 0.1\n\nfrom typing import Iterable, Hashable, List, Tuple, Dict\n\nclass UnionFind:\n    def __init__(self, items: Iterable[Hashable]):\n        self.parent = {x: x for x in items}\n        self.rank = {x: 0 for x in items}\n\n    def find(self, x: Hashable) -> Hashable:\n        # Path compression\n        while self.parent[x] != x:\n            self.parent[x] = self.parent[self.parent[x]]\n            x = self.parent[x]\n        return x\n\n    def union(self, a: Hashable, b: Hashable) -> None:\n        ra, rb = self.find(a), self.find(b)\n        if ra == rb:\n            return\n        # Union by rank\n        if self.rank[ra] < self.rank[rb]:\n            self.parent[ra] = rb\n        elif self.rank[ra] > self.rank[rb]:\n            self.parent[rb] = ra\n        else:\n            self.parent[rb] = ra\n            self.rank[ra] += 1\n\ndef connected_components_from_pairs(\n    nodes: Iterable[Hashable],\n    positive_pairs: Iterable[Tuple[Hashable, Hashable]],\n) -> List[List[Hashable]]:\n    nodes = list(nodes)\n    uf = UnionFind(nodes)\n\n    for a, b in positive_pairs:\n        uf.union(a, b)\n\n    groups: Dict[Hashable, List[Hashable]] = {}\n    for x in nodes:\n        root = uf.find(x)\n        groups.setdefault(root, []).append(x)\n\n    return list(groups.values())\n\ndef merge_masks(masks):\n    connected_pairs = []\n    for i in range(len(masks)-1):\n        for j in range(i+1, len(masks)):\n            if masks_intersect(masks[i], masks[j]):\n                connected_pairs.append((i, j))\n\n    components = connected_components_from_pairs(range(len(masks)), connected_pairs)\n\n    res = []\n    for comp in components:\n        comp_mask = np.zeros_like(masks[0])\n        for ind in comp:\n            comp_mask = np.logical_or(comp_mask, masks[ind])\n        res.append(comp_mask)\n\n    return res","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def box_to_rect(box, max_x, max_y):\n    x_min = np.clip(int(min(box[:, 0])), 0, max_x)\n    x_max = np.clip(int(max(box[:, 0])), 0, max_x)\n    y_min = np.clip(int(min(box[:, 1])), 0, max_y)\n    y_max = np.clip(int(max(box[:, 1])), 0, max_y)\n    return x_min, x_max, y_min, y_max\n\ndef rle_detected_mask(img_path, debug=False):\n    tiles = detect_tiles(img_path)\n    img = load_image(img_path)\n    img_cv2 = cv2.imread(img_path)\n    \n    if debug:\n        viz2d.plot_images([img])\n    \n    masks = []\n    \n    features = {}\n    \n    for i in range(len(tiles)-1):\n        if i not in features:\n            x, y, w, h = tiles[i]\n            features[i] = extractor.extract(\n                img[:, y:(y+h), x:(x+w)].to(device)\n            )\n            text_mask = db50_text_location(img_cv2[y:(y+h), x:(x+w)])\n            features[i] = filter_keypoints_by_mask(features[i], text_mask)\n            \n        for j in range(i+1, len(tiles)):\n            if j not in features:\n                x, y, w, h = tiles[j]\n                features[j] = extractor.extract(\n                    img[:, y:(y+h), x:(x+w)].to(device)\n                )\n                text_mask = db50_text_location(img_cv2[y:(y+h), x:(x+w)])\n                features[j] = filter_keypoints_by_mask(features[j], text_mask)\n                \n            if debug:\n                start = time.time()\n            \n            match_res = detect_matches(features[i], features[j], reproj_thresh=AFFINE_REPROJ_THRESH)\n            \n            if debug:\n                end = time.time()\n                \n            if not match_res:\n                if debug:\n                    print(f\"{i} - {j}, {end-start:.3f}s  - no match\")\n            else:\n                if debug:\n                    print(f\"{i} - {j}, {end-start:.3f}s - match {match_res['box0']} <-> {match_res['box1']}\")\n    \n                mask = np.zeros(img.shape[1:], dtype='int8')\n                \n                x0, y0, w0, h0 = tiles[i]\n                x_min, x_max, y_min, y_max = box_to_rect(match_res['box0'], w0, h0)\n                mask[y0+y_min: y0+y_max, x0+x_min: x0+x_max] = 1\n                \n                x1, y1, w1, h1 = tiles[j]\n                x_min, x_max, y_min, y_max = box_to_rect(match_res['box1'], w1, h1)\n                mask[y1+y_min: y1+y_max, x1 + x_min: x1+x_max] = 1\n    \n                masks.append(mask)\n\n                if debug:\n                    show_matching_boxes(\n                        img[:, y0:(y0+h0), x0:(x0+w0)],\n                        img[:, y1:(y1+h1), x1:(x1+w1)],\n                        match_res\n                    )\n\n    masks = merge_masks(masks)\n\n    if len(masks) == 0:\n        return 'authentic'\n    else:\n        return metric.rle_encode(masks)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T00:01:05.644207Z","iopub.execute_input":"2025-12-30T00:01:05.644514Z","iopub.status.idle":"2025-12-30T00:01:05.653989Z","shell.execute_reply.started":"2025-12-30T00:01:05.644488Z","shell.execute_reply":"2025-12-30T00:01:05.653283Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(dir, filenames):\n    case_ids = []\n    annotations = []\n    for f in filenames:\n        img_path = dir + f\n        annotation = rle_detected_mask(img_path)\n\n        case_ids.append(f.split('.')[0])\n        annotations.append(annotation)\n        \n    return pd.DataFrame({\n        'case_id': case_ids, \n        'annotation': annotations\n    })","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T00:01:08.089132Z","iopub.execute_input":"2025-12-30T00:01:08.08986Z","iopub.status.idle":"2025-12-30T00:01:08.09391Z","shell.execute_reply.started":"2025-12-30T00:01:08.089831Z","shell.execute_reply":"2025-12-30T00:01:08.093253Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def score(img_dir, mask_dir, ids):\n    img_filenames = [str(id)+'.png' for id in ids]\n    submission = predict(img_dir, img_filenames).set_index('case_id')\n\n    case_ids = []\n    shapes = []\n    annotations = []\n    for id in ids:\n        id = str(id)\n        case_ids.append(id)\n        mask_file = id + '.npy'\n        masks = np.load(mask_dir + mask_file)\n        shapes.append(str(list(masks[0].shape)))\n        annotations.append(metric.rle_encode(masks))\n\n    solution = pd.DataFrame({\n        'case_id': case_ids, \n        'annotation': annotations, \n        'shape': shapes\n    }).set_index('case_id')\n\n    return metric.score(solution, submission, row_id_column_name='not_important')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T00:01:09.67127Z","iopub.execute_input":"2025-12-30T00:01:09.671583Z","iopub.status.idle":"2025-12-30T00:01:09.677245Z","shell.execute_reply.started":"2025-12-30T00:01:09.671546Z","shell.execute_reply":"2025-12-30T00:01:09.676577Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"img_dir = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images/\"\nfiles = os.listdir(img_dir)\nsubmission = predict(img_dir, files)\nsubmission.to_csv('submission.csv', index=False)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}