{"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":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":113558,"databundleVersionId":14878066}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"I packaged the solution as a Python library and published the model weights through a GitHub Release.\n\nCheck out the [vlad3996/forgeryscope repository](https://github.com/vlad3996/forgeryscope) for installation instructions, examples, and pretrained weights.\n\nSingle-image inference example is available here:\n[examples/quick_start.ipynb](https://github.com/vlad3996/forgeryscope/blob/main/examples/quick_start.ipynb). ","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"!pip install -qq forgeryscope git+https://github.com/cvg/LightGlue.git","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T14:56:09.909150Z","iopub.execute_input":"2026-04-29T14:56:09.909334Z","iopub.status.idle":"2026-04-29T14:56:23.553101Z","shell.execute_reply.started":"2026-04-29T14:56:09.909313Z","shell.execute_reply":"2026-04-29T14:56:23.552372Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom ultralytics import YOLO\n\nfrom forgeryscope import Embedder, PanelExtractor, get_model_path, load_aliked_wblot_weights\nfrom forgeryscope.matcher.geometry import get_intersections\nfrom forgeryscope.matcher.lane import find_lanes_in_blot_panels, create_lane_match_masks\nfrom forgeryscope.matcher.lightglue import LightGlueOverlap, create_duplicate_masks, merge_masks_by_max_cliques\nfrom forgeryscope.matcher.plot import visualize_duplicate_masks","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T14:56:25.344808Z","iopub.execute_input":"2026-04-29T14:56:25.345198Z","iopub.status.idle":"2026-04-29T14:56:41.415917Z","shell.execute_reply.started":"2026-04-29T14:56:25.345157Z","shell.execute_reply":"2026-04-29T14:56:41.415291Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DEVICE = \"cuda\"\nPRINT_MODEL_DEFINITION = True\nVERBOSE = False\n\npanel_extractor = PanelExtractor(\n    weights_path=\"yolo_panel_extractor\",\n    device=DEVICE,\n    conf_threshold=0.7,\n    iou_threshold=0.4,\n    verbose=False,\n)\npanel_extractor.EXCLUDED_LABELS = {\"Graphs\", \"Flow Cytometry\", \"Body Imaging\"}\n\nlane_extractor = YOLO(get_model_path(\"yolo_lane_extractor\"))\n\nwblot_duplicate_embedder = Embedder(\"wblot_duplicate_embedder\", device=DEVICE, verbose=PRINT_MODEL_DEFINITION)\nwblot_overlap_embedder = Embedder(\"wblot_overlap_embedder\", device=DEVICE, verbose=PRINT_MODEL_DEFINITION)\nwblot_lane_embedder = Embedder(\"wblot_lane_embedder\", device=DEVICE, verbose=PRINT_MODEL_DEFINITION)\nmicro_overlap_embedder = Embedder(\"micro_overlap_embedder\", device=DEVICE, verbose=PRINT_MODEL_DEFINITION)\n\nmatcher_micro = LightGlueOverlap(\n    max_keypoints=4096,\n    matcher_features=\"sift\",\n    device=DEVICE,\n    depth_confidence=0.9,\n    width_confidence=0.9,\n    verbose=PRINT_MODEL_DEFINITION,\n)\n\nmatcher_blot = LightGlueOverlap(\n    max_keypoints=512,\n    matcher_features=\"aliked\",\n    device=DEVICE,\n    depth_confidence=-1,\n    width_confidence=-1,\n    estimator_method=\"MAGSAC\",\n    reprojThreshold=3.0,\n    estimator_confidence=0.9999,\n    estimator_maxIters=5000,\n    estimator_refineIters=10,\n    verbose=PRINT_MODEL_DEFINITION,\n)\nload_aliked_wblot_weights(matcher_blot);","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T14:56:41.417225Z","iopub.execute_input":"2026-04-29T14:56:41.417678Z","iopub.status.idle":"2026-04-29T14:57:06.472820Z","shell.execute_reply.started":"2026-04-29T14:56:41.417647Z","shell.execute_reply":"2026-04-29T14:57:06.471887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"MATCH_SCORE_THRESHOLD = 0.73\nINLIER_THRESHOLD = 8\nMATCH_FILTER_STR = 'mean_match_score'\n\nWBLOT_DUP_SCORE_THRESH = 0.84\nMICROSCOPY_EMB_THRESH = 0.58\nMICRO_DUP_SCORE_THRESH = 0.85\nWBLOT_OVERLAP_THRESHOLD = 0.85\nSEG_SIM_THRESH = 0.65\n\npathes = [\n    \"/kaggle/input/competitions/recodai-luc-scientific-image-forgery-detection/test_images/45.png\", \n    \"/kaggle/input/competitions/recodai-luc-scientific-image-forgery-detection/supplemental_images/20833.png\"\n]\n\nfor path in pathes:\n    print(path)\n    mask_matcher = []\n    mask_lanes = []\n    merged_info = []\n    blot_panels_ids = []\n    similar_pairs_blot = []\n    try:\n        if path.startswith('http'):\n            img = load_image_from_url(path)\n        else:\n            img = PanelExtractor._load_image(path)\n        panels = panel_extractor.extract_panels(img)\n        intersections = get_intersections(panels, margin=10)\n\n        crops_list = PanelExtractor.crop_panels(img, panels)\n\n        blot_panels_ids = [i for i in range(len(panels)) if panels[i][0] == 'Blots']\n        microscopy_panels_ids = [i for i in range(len(panels)) if panels[i][0] == 'Microscopy']\n\n        # Compute similarities for western blot panels:\n        if len(blot_panels_ids):\n            crops_list_blot = PanelExtractor.crop_panels(img, [panels[i] for i in blot_panels_ids])\n            embeddings_blot_overlap = wblot_overlap_embedder.get_embedding_batch(crops_list_blot).cpu()\n            pairs_overlap = [\n                (\n                    blot_panels_ids[i],  # source index in panels\n                    blot_panels_ids[j],\n                    score\n                )\n                for i, j, score in Embedder.find_similar_pairs(embeddings_blot_overlap, threshold=WBLOT_OVERLAP_THRESHOLD)\n            ]\n            embeddings_blot_duplicate = wblot_duplicate_embedder.get_embedding_batch(crops_list_blot).cpu()\n            pairs_duplicate = [\n                (\n                    blot_panels_ids[i],  # source index in panels\n                    blot_panels_ids[j],\n                    score\n                )\n                for i, j, score in Embedder.find_similar_pairs(embeddings_blot_duplicate, threshold=WBLOT_DUP_SCORE_THRESH)\n            ]\n            print('pairs_overlap :', len(pairs_overlap), 'pairs_duplicate :', len(pairs_duplicate))\n            pairs_dict = {}\n            for i, j, score in pairs_overlap + pairs_duplicate:\n                key = tuple(sorted((i, j)))  # Normalize pair order\n                if key not in pairs_dict or score > pairs_dict[key]:\n                    pairs_dict[key] = score\n\n            similar_pairs_blot = [\n                ('Blots', score, i, j)\n                for (i, j), score in pairs_dict.items()\n            ]\n        else:\n            similar_pairs_blot = []\n\n        # Compute similarities for microscopy panels:\n        if len(microscopy_panels_ids):\n            crops_list_microscopy = PanelExtractor.crop_panels(img, [panels[i] for i in microscopy_panels_ids])\n            embeddings_microscopy = micro_overlap_embedder.get_embedding_batch(crops_list_microscopy).cpu()\n            similar_pairs_microscopy = [\n                (\n                    'Microscopy',\n                    score,\n                    microscopy_panels_ids[i],  # source index in panels\n                    microscopy_panels_ids[j]\n                )\n                for i, j, score in Embedder.find_similar_pairs(embeddings_microscopy, threshold=MICROSCOPY_EMB_THRESH)\n            ]\n        else:\n            similar_pairs_microscopy = []\n\n        similar_pairs = similar_pairs_blot + similar_pairs_microscopy\n        similar_pairs[:] = [\n            (label, score, i, j)\n            for (label, score, i, j) in similar_pairs\n            if (i, j) not in intersections and (j, i) not in intersections\n        ]\n\n        clf_predicts = pd.DataFrame(similar_pairs, columns=['label', 'score', 'idx1', 'idx2'])\n\n        match_results = create_duplicate_masks(\n            img,\n            panels,\n            crops_list,\n            clf_predicts,\n            matcher_micro,\n            matcher_blot,\n            to_bbox_micro=False,\n            to_bbox_blot=True,\n            fallback_for_wblot=True,\n            test_transforms_blot=False,\n            test_transforms_micro=True,\n        )\n        # print('valid results from matcher :', len(match_results))\n\n        pred_masks, duplicate_info = [], []\n        for info in match_results:\n            id0 = info['panel_id0']\n            id1 = info['panel_id1']\n            label = info['panel_label']\n            match_result = info['match_result']\n            inliers = match_result[\"inliers\"]\n            matcher_model_score = match_result[MATCH_FILTER_STR]\n            if label == 'Blots':\n                if inliers < INLIER_THRESHOLD or matcher_model_score < MATCH_SCORE_THRESHOLD:\n                    if VERBOSE:\n                        print(f\"-----4. Keeping BLOT pair despite low score/inliers: ({id0}, {id1}), {inliers} inliers, {matcher_model_score:.3f}\")\n                instance_mask = (info['mask0'] | info['mask1']).astype(np.uint8)\n                pred_masks.append(instance_mask)\n                duplicate_info.append(info)\n                continue\n\n            # Microscopy filtering logic\n            if (inliers >= INLIER_THRESHOLD) and (matcher_model_score < MATCH_SCORE_THRESHOLD):\n                # Only one condition is bad (good inliers, bad score) - check micro deduplicator\n                bbox_crop0 = info['bbox_crop0']\n                bbox_crop1 = info['bbox_crop1']\n                img0 = crops_list[id0]\n                img1 = crops_list[id1]\n                img0_crop = Image.fromarray(img0).crop(bbox_crop0)\n                img1_crop = Image.fromarray(img1).crop(bbox_crop1)\n                micro_score = micro_overlap_embedder.compare(img0_crop, img1_crop)\n\n                if micro_score < MICRO_DUP_SCORE_THRESH:\n                    if VERBOSE:\n                        print(f\"-----1. Skipping pair ({id0}, {id1}) due to low match score + low sim score: {matcher_model_score:.3f} AND MicroDuplicate sim score: {micro_score:.3f}. BTW: inliers={inliers}\")\n                    continue\n                else:\n                    if VERBOSE:\n                        print(f\"-----2. Keep pair ({id0}, {id1}): inliers={inliers}; match score {matcher_model_score:.3f}(LOW) +++ MicroDuplicate sim score: {micro_score:.3f} (HIGH)\")\n            elif (inliers < INLIER_THRESHOLD) or (matcher_model_score < MATCH_SCORE_THRESHOLD):\n                if VERBOSE:\n                    print(f\"-----3. Skipping pair ({id0}, {id1}) due to low {inliers} inliers or matcher_model_score: {matcher_model_score:.3f}.\")\n                continue\n\n            instance_mask = (info['mask0'] | info['mask1']).astype(np.uint8)\n            pred_masks.append(instance_mask)\n            duplicate_info.append(info)\n        mask_matcher, merged_info = merge_masks_by_max_cliques(pred_masks, duplicate_info, verbose=VERBOSE)\n    except Exception as ex:\n        print(ex)\n        continue\n\n    if len(blot_panels_ids) > 0 and not len(similar_pairs_blot):\n        lane_match_result = find_lanes_in_blot_panels(\n            panels=panels,\n            blot_panels_ids=blot_panels_ids,\n            crops_list=crops_list,\n            segmentator=lane_extractor,\n            blot_duplicate_detector=wblot_lane_embedder,\n            similarity_threshold=SEG_SIM_THRESH,\n            overlap_threshold=5\n        )\n        if len(lane_match_result):\n            mask_lanes = create_lane_match_masks(img.shape, lane_match_result['best_matches'], lanes=lane_match_result['lanes'])\n\n            if len(lane_match_result['best_matches']):\n                plt.figure(figsize=(15, 15))\n                plt.imshow(visualize_matches_on_image(img, lane_match_result['best_matches']))\n                plt.axis('off')\n                plt.show()\n\n    mask_matcher = mask_matcher + mask_lanes\n\n    if not len(mask_matcher):\n        annotation = \"authentic\"\n    else:\n        annotation = mask_matcher\n\n    # annotation -- final prediction\n\n    summary = visualize_duplicate_masks(\n        img,\n        mask_matcher,\n        merged_info,\n        fallback_masks=None,\n        fallback_info=None,\n        show_fallbacks=False,\n        # save_path='vis/' + path.split('/')[-1].replace('.png', '_vis.png'), dpi=150\n    )\n    print('='*80)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T14:57:06.473848Z","iopub.execute_input":"2026-04-29T14:57:06.474256Z","iopub.status.idle":"2026-04-29T14:57:32.662094Z","shell.execute_reply.started":"2026-04-29T14:57:06.474227Z","shell.execute_reply":"2026-04-29T14:57:32.660970Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}