{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.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":115267,"databundleVersionId":13761094},{"sourceType":"datasetVersion","sourceId":7884485,"datasetId":4628051,"databundleVersionId":7990559},{"sourceType":"datasetVersion","sourceId":11924468,"datasetId":6988459,"databundleVersionId":12432458},{"sourceType":"modelInstanceVersion","sourceId":17555,"databundleVersionId":7980546,"modelInstanceId":14611,"modelId":22086},{"sourceType":"modelInstanceVersion","sourceId":4534,"databundleVersionId":6346558,"modelInstanceId":3326,"modelId":986},{"sourceType":"modelInstanceVersion","sourceId":17191,"databundleVersionId":7971915,"modelInstanceId":14317,"modelId":21716}],"dockerImageVersionId":30919,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Install dependencies and copy model weights for OFFLINE execution\n!pip install --no-index /kaggle/input/imc2024-packages-lightglue-rerun-kornia/* --no-deps\n!mkdir -p /root/.cache/torch/hub/checkpoints\n!cp /kaggle/input/aliked/pytorch/aliked-n16/1/aliked-n16.pth /root/.cache/torch/hub/checkpoints/\n!cp /kaggle/input/lightglue/pytorch/aliked/1/aliked_lightglue.pth /root/.cache/torch/hub/checkpoints/\n!cp /kaggle/input/lightglue/pytorch/aliked/1/aliked_lightglue.pth /root/.cache/torch/hub/checkpoints/aliked_lightglue_v0-1_arxiv-pth","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:52:38.202240Z","iopub.execute_input":"2026-05-10T18:52:38.202578Z","iopub.status.idle":"2026-05-10T18:52:44.279482Z","shell.execute_reply.started":"2026-05-10T18:52:38.202549Z","shell.execute_reply":"2026-05-10T18:52:44.278287Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\nwarnings.simplefilter(action='ignore', category=FutureWarning)\n\nimport sys, os, gc, dataclasses\nfrom tqdm import tqdm\nfrom time import time, sleep\nfrom copy import deepcopy\nimport numpy as np\nimport pandas as pd\nimport h5py\nimport torch\nimport torch.nn.functional as F\nimport kornia as K\nimport kornia.feature as KF\nimport torchvision.transforms.functional as TF\nfrom PIL import Image\n\nfrom lightglue import ALIKED\nfrom transformers import AutoImageProcessor, AutoModel\n\nimport pycolmap\nsys.path.append('/kaggle/input/imc25-utils')\nfrom database import COLMAPDatabase\nfrom h5_to_db import add_keypoints, add_matches\n\n# =========================\n# MAX QUALITY CONFIGURATION\n# =========================\nMAX_FEATURES = 8192\nIMG_RESIZE = 1536\nMIN_PAIRS = 15\nMAX_PAIRS_PER_IMG = 50  \nMAX_MODELS = 50\n\n# =========================\n# DATA STRUCT & BASE CONFIG\n# =========================\n@dataclasses.dataclass\nclass Prediction:\n    image_id: str | None\n    dataset: str\n    filename: str\n    cluster_index: int | None = None\n    rotation: np.ndarray | None = None\n    translation: np.ndarray | None = None\n\ndata_dir = '/kaggle/input/competitions/image-matching-challenge-2025-ongoing'\nworkdir = '/kaggle/working/result/'\nos.makedirs(workdir, exist_ok=True)\n\n# Hardcoded for the competition submission file\nsample_csv = f'{data_dir}/sample_submission.csv'\nsamples = {}\ndf = pd.read_csv(sample_csv)\n\nfor _, r in df.iterrows():\n    samples.setdefault(r.dataset, []).append(Prediction(r.image_id, r.dataset, r.image))\n\n# =========================\n# DEVICE SETUP\n# =========================\ndevice = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\nprint(\"Using device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:52:44.280872Z","iopub.execute_input":"2026-05-10T18:52:44.281233Z","iopub.status.idle":"2026-05-10T18:53:07.680176Z","shell.execute_reply.started":"2026-05-10T18:52:44.281196Z","shell.execute_reply":"2026-05-10T18:53:07.679278Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# IO & CORRECTION\n# =========================\ndef load_torch_image(fname, device=torch.device('cpu')):\n    return K.io.load_image(fname, K.io.ImageLoadType.RGB32, device=device)[None, ...]\n\ndef rotate_image(image, angle):\n    return TF.rotate(image, angle)\n\ndef correct_and_save_image(img_path, output_path):\n    pil_img = Image.open(img_path).convert(\"RGB\")\n    rotations = [0, 90, 180, 270]\n    best_img = pil_img\n    best_angle = 0\n    for angle in rotations:\n        rotated = rotate_image(pil_img, angle)\n        if angle == 0:\n            best_img = rotated\n            best_angle = angle\n    best_img.save(output_path)\n    return best_angle\n\n# =========================\n# GLOBAL DESC\n# =========================\ndef get_global_desc(fnames, device):\n    processor = AutoImageProcessor.from_pretrained('/kaggle/input/dinov2/pytorch/base/1')\n    model = AutoModel.from_pretrained('/kaggle/input/dinov2/pytorch/base/1').to(device).eval()\n    descs = []\n    valid_fnames = []\n    \n    for img_path in fnames:\n        try:\n            img = load_torch_image(img_path, device=device)\n            with torch.inference_mode():\n                inputs = processor(images=img, return_tensors=\"pt\", do_rescale=False).to(device)\n                outputs = model(**inputs)\n                d = F.normalize(outputs.last_hidden_state[:,1:].max(dim=1)[0], dim=1, p=2)\n            descs.append(d.cpu())\n            valid_fnames.append(img_path)\n        except Exception:\n            print(f\"\\nSkipping corrupted image: {img_path}\")\n            continue\n            \n    del processor, model\n    torch.cuda.empty_cache()\n    \n    fnames[:] = valid_fnames\n    return torch.cat(descs, 0) if descs else torch.empty(0)\n\ndef get_img_pairs_exhaustive(img_fnames):\n    return [(i, j) for i in range(len(img_fnames)) for j in range(i+1, len(img_fnames))]\n\ndef get_image_pairs_shortlist(fnames, sim_th=0.25, min_pairs=15, max_pairs_per_img=50, exhaustive_if_less=30, device=None):\n    num_imgs = len(fnames)\n    if num_imgs <= exhaustive_if_less:\n        return get_img_pairs_exhaustive(fnames)\n    descs = get_global_desc(fnames, device=device)\n    if len(descs) == 0:\n        return []\n    dm = torch.cdist(descs, descs, p=2).cpu().numpy()\n    mask = dm <= sim_th\n    pairs = []\n    ar = np.arange(len(fnames))\n    for i in range(len(fnames)-1):\n        idxs = ar[mask[i]]\n        if len(idxs) < min_pairs:\n            idxs = np.argsort(dm[i])[:min_pairs]\n        if len(idxs) > max_pairs_per_img:\n            idxs = np.argsort(dm[i])[:max_pairs_per_img]\n        for j in idxs:\n            if i != j and dm[i, j] < 1000:\n                pairs.append(tuple(sorted((i, j.item()))))\n    del descs, dm, mask\n    return sorted(list(set(pairs)))\n\n# =========================\n# FEATURE EXTRACTION\n# =========================\ndef detect_aliked(img_fnames, feature_dir, num_features, resize_to, device):\n    dtype = torch.float32 \n    extractor = ALIKED(max_num_keypoints=num_features, detection_threshold=0.05, resize=resize_to).to(device, dtype).eval()\n    os.makedirs(feature_dir, exist_ok=True)\n    os.makedirs(os.path.join(feature_dir, 'corrected'), exist_ok=True)\n    with h5py.File(f'{feature_dir}/keypoints.h5','w') as f_kp, h5py.File(f'{feature_dir}/descriptors.h5','w') as f_desc:\n        for img_path in tqdm(img_fnames, desc=\"Detecting Features\"):\n            key = img_path.split('/')[-1]\n            corrected_path = os.path.join(feature_dir, 'corrected', key)\n            try:\n                if not os.path.exists(corrected_path):\n                    correct_and_save_image(img_path, corrected_path)\n                with torch.inference_mode():\n                    img = load_torch_image(corrected_path, device=device).to(dtype)\n                    feats = extractor.extract(img)\n                    f_kp[key] = feats['keypoints'].reshape(-1, 2).cpu().numpy()\n                    f_desc[key] = feats['descriptors'].reshape(len(feats['keypoints'][0]), -1).cpu().numpy()\n            except Exception:\n                continue\n    del extractor\n    if 'feats' in locals(): del feats\n    if 'img' in locals(): del img\n    torch.cuda.empty_cache()\n    gc.collect()\n\n# =========================\n# MATCHING\n# =========================\ndef match_with_lightglue(img_fnames, index_pairs, feature_dir, device, min_matches=12):\n    if not index_pairs: return\n    matcher = KF.LightGlueMatcher(\"aliked\", {\"width_confidence\": -1, \"depth_confidence\": -1, \"mp\": True if 'cuda' in str(device) else False}).to(device).eval()\n    kp_dict = {}\n    desc_dict = {}\n    with h5py.File(f'{feature_dir}/keypoints.h5','r') as f_kp, h5py.File(f'{feature_dir}/descriptors.h5','r') as f_desc:\n        for fname in img_fnames:\n            k = fname.split('/')[-1]\n            if k in f_kp and k in f_desc:\n                kp_dict[k] = torch.from_numpy(f_kp[k][...]).to(device)\n                desc_dict[k] = torch.from_numpy(f_desc[k][...]).to(device)\n    matches_out = {}\n    for i, j in tqdm(index_pairs, desc=\"Matching Features\"):\n        k1, k2 = img_fnames[i].split('/')[-1], img_fnames[j].split('/')[-1]\n        if k1 not in kp_dict or k2 not in kp_dict: continue\n        kp1, kp2 = kp_dict[k1], kp_dict[k2]\n        d1, d2 = desc_dict[k1], desc_dict[k2]\n        with torch.inference_mode():\n            _, idxs = matcher(d1, d2, KF.laf_from_center_scale_ori(kp1[None]), KF.laf_from_center_scale_ori(kp2[None]))\n        if len(idxs) >= min_matches:\n            matches_out[(k1, k2)] = idxs.cpu().numpy().reshape(-1, 2)\n    with h5py.File(f'{feature_dir}/matches.h5','w') as f_match:\n        for (k1, k2), idxs in matches_out.items():\n            group = f_match.require_group(k1)\n            group.create_dataset(k2, data=idxs)\n    del matcher, kp_dict, desc_dict, matches_out\n    torch.cuda.empty_cache()\n    gc.collect()\n\n# =========================\n# COLMAP\n# =========================\ndef import_into_colmap(img_dir, feature_dir, db_path):\n    db = COLMAPDatabase.connect(db_path)\n    db.create_tables()\n    fname_to_id = add_keypoints(db, feature_dir, img_dir, '', 'simple-pinhole', False)\n    add_matches(db, feature_dir, fname_to_id)\n    db.commit()\n\n# =========================\n# PIPELINE\n# =========================\ndef process_dataset(dataset, preds, device):\n    maps = None\n    images_dir = os.path.join(data_dir, 'test', dataset)\n    \n    raw_images = [os.path.join(images_dir, p.filename) for p in preds]\n    images = []\n    \n    print(f\"\\n[{dataset}] Checking image integrity...\")\n    for img in raw_images:\n        try:\n            Image.open(img).verify()\n            images.append(img)\n        except Exception:\n            print(f\"Skipping corrupted image: {img}\")\n            \n    if not images:\n        print(f\"[{dataset}] No valid images found. Skipping.\")\n        return\n\n    feature_dir = os.path.join(workdir, 'featureout', dataset)\n    os.makedirs(feature_dir, exist_ok=True)\n    try:\n        print(f\"[{dataset}] Starting processing ({len(images)} images)...\")\n        t0 = time()\n        \n        pairs = get_image_pairs_shortlist(images, sim_th=0.25, min_pairs=MIN_PAIRS, max_pairs_per_img=MAX_PAIRS_PER_IMG, device=device)\n        print(f\"[{dataset}] Shortlisted {len(pairs)} pairs in {time()-t0:.1f}s\")\n        if not pairs:\n            print(f\"[{dataset}] No valid pairs found, skipping reconstruction.\")\n            return\n            \n        detect_aliked(images, feature_dir, MAX_FEATURES, IMG_RESIZE, device)\n        match_with_lightglue(images, pairs, feature_dir, device)\n        \n        db_path = f'{feature_dir}/colmap.db'\n        if os.path.exists(db_path): os.remove(db_path)\n        \n        print(f\"[{dataset}] Running PyColmap reconstruction...\")\n        import_into_colmap(images_dir, feature_dir, db_path)\n        pycolmap.match_exhaustive(db_path)\n        \n        mapper = pycolmap.IncrementalPipelineOptions()\n        mapper.min_model_size = 3\n        mapper.max_num_models = MAX_MODELS\n        mapper.num_threads = 4 \n        out_path = f'{feature_dir}/recon'\n        os.makedirs(out_path, exist_ok=True)\n        maps = pycolmap.incremental_mapping(db_path, images_dir, out_path, options=mapper)\n        \n        registered = 0\n        for map_index, m in maps.items():\n            for _, img in m.images.items():\n                try:\n                    idx = next(i for i,p in enumerate(preds) if p.filename == img.name)\n                    preds[idx].cluster_index = map_index\n                    preds[idx].rotation = deepcopy(img.cam_from_world.rotation.matrix())\n                    preds[idx].translation = deepcopy(img.cam_from_world.translation)\n                    registered += 1\n                except StopIteration:\n                    continue\n        print(f\"[{dataset}] Finished successfully! Registered {registered}/{len(images)} images.\")\n    except Exception as e:\n        print(f\"[{dataset}] Failed: {e}\")\n    finally:\n        gc.collect()\n        return maps\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:53:07.681943Z","iopub.execute_input":"2026-05-10T18:53:07.682251Z","iopub.status.idle":"2026-05-10T18:53:07.712210Z","shell.execute_reply.started":"2026-05-10T18:53:07.682226Z","shell.execute_reply":"2026-05-10T18:53:07.711194Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# EXECUTION\n# =========================\nall_maps = {}\n\nfor dataset, preds in samples.items():\n    maps = process_dataset(dataset, preds, device)\n    all_maps[dataset] = maps\n\n# =========================\n# SUBMISSION\n# =========================\narray_to_str = lambda a: ';'.join([f\"{x:.09f}\" for x in a])\nnone_to_str = lambda n: ';'.join(['nan'] * n)\nout_file = '/kaggle/working/submission.csv'\n\nwith open(out_file, 'w') as f:\n    f.write('image_id,dataset,scene,image,rotation_matrix,translation_vector\\n')\n    for ds in samples:\n        for p in samples[ds]:\n            cluster = 'outliers' if p.cluster_index is None else f'cluster{p.cluster_index}'\n            R = none_to_str(9) if p.rotation is None else array_to_str(p.rotation.flatten())\n            T = none_to_str(3) if p.translation is None else array_to_str(p.translation)\n            f.write(f'{p.image_id},{p.dataset},{cluster},{p.filename},{R},{T}\\n')\n\nprint(\"\\nSaved:\", out_file)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:53:07.713740Z","iopub.execute_input":"2026-05-10T18:53:07.714175Z","iopub.status.idle":"2026-05-10T18:56:57.119861Z","shell.execute_reply.started":"2026-05-10T18:53:07.714134Z","shell.execute_reply":"2026-05-10T18:56:57.118881Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# VISUALIZATION UTILITIES\n# =========================\nimport matplotlib.pyplot as plt\nimport cv2\nimport numpy as np\nimport h5py\nfrom collections import Counter\n\n# 🔍 Keypoints\ndef visualize_keypoints(image_path, feature_dir, max_kp=500):\n    img = cv2.imread(image_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n    key = image_path.split('/')[-1]\n\n    with h5py.File(f'{feature_dir}/keypoints.h5','r') as f_kp:\n        if key not in f_kp:\n            print(\"No keypoints found\")\n            return\n        kpts = f_kp[key][...]\n\n    kpts = kpts[:max_kp]\n\n    plt.figure(figsize=(6,6))\n    plt.imshow(img)\n    plt.scatter(kpts[:,0], kpts[:,1], s=5)\n    plt.title(f\"{key} - {len(kpts)} keypoints\")\n    plt.axis('off')\n    plt.show()\n\n\n# 🔗 Matches\ndef visualize_matches(img1_path, img2_path, feature_dir, max_matches=50):\n    img1 = cv2.imread(img1_path)\n    img2 = cv2.imread(img2_path)\n    img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)\n    img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)\n\n    k1 = img1_path.split('/')[-1]\n    k2 = img2_path.split('/')[-1]\n\n    with h5py.File(f'{feature_dir}/keypoints.h5','r') as f_kp, \\\n         h5py.File(f'{feature_dir}/matches.h5','r') as f_m:\n\n        if k1 not in f_m or k2 not in f_m[k1]:\n            print(\"No matches found\")\n            return\n\n        kp1 = f_kp[k1][...]\n        kp2 = f_kp[k2][...]\n        matches = f_m[k1][k2][...]\n\n    matches = matches[:max_matches]\n\n    h1, w1 = img1.shape[:2]\n    h2, w2 = img2.shape[:2]\n\n    canvas = np.zeros((max(h1,h2), w1+w2, 3), dtype=np.uint8)\n    canvas[:h1, :w1] = img1\n    canvas[:h2, w1:] = img2\n\n    plt.figure(figsize=(12,6))\n    plt.imshow(canvas)\n\n    for i,j in matches:\n        x1,y1 = kp1[i]\n        x2,y2 = kp2[j]\n        plt.plot([x1, x2+w1], [y1, y2], linewidth=0.5)\n\n    plt.axis('off')\n    plt.title(f\"{k1} ↔ {k2} ({len(matches)} matches)\")\n    plt.show()\n\n\n# 📊 Match count histogram\ndef plot_match_histogram(feature_dir):\n    with h5py.File(f'{feature_dir}/matches.h5','r') as f:\n        counts = []\n        for k1 in f.keys():\n            for k2 in f[k1].keys():\n                counts.append(len(f[k1][k2]))\n\n    plt.hist(counts, bins=30)\n    plt.title(\"Matches per Pair\")\n    plt.xlabel(\"Matches\")\n    plt.ylabel(\"Frequency\")\n    plt.show()\n\n\n# 🔗 Connectivity\ndef plot_connectivity(pairs):\n    count = Counter()\n    for i,j in pairs:\n        count[i]+=1\n        count[j]+=1\n\n    plt.hist(list(count.values()), bins=20)\n    plt.title(\"Connections per Image\")\n    plt.xlabel(\"Connections\")\n    plt.show()\n\n\n# 🧱 Reconstruction summary\ndef plot_reconstruction_summary(maps):\n    sizes = [len(m.images) for m in maps.values()]\n\n    plt.bar(range(len(sizes)), sizes)\n    plt.title(\"Images per Cluster\")\n    plt.xlabel(\"Cluster\")\n    plt.ylabel(\"Num Images\")\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:56:57.120889Z","iopub.execute_input":"2026-05-10T18:56:57.121191Z","iopub.status.idle":"2026-05-10T18:56:57.135442Z","shell.execute_reply.started":"2026-05-10T18:56:57.121167Z","shell.execute_reply":"2026-05-10T18:56:57.134573Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# DEBUG VISUALIZATION RUNNER\n# =========================\n\ndataset = list(samples.keys())[0]  # pick first dataset\nfeature_dir = os.path.join(workdir, 'featureout', dataset)\nimages_dir = os.path.join(data_dir, 'test', dataset)\n\nimages = [os.path.join(images_dir, p.filename) for p in samples[dataset]]\n\nprint(\"Using dataset:\", dataset)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:56:57.136623Z","iopub.execute_input":"2026-05-10T18:56:57.136956Z","iopub.status.idle":"2026-05-10T18:56:57.154780Z","shell.execute_reply.started":"2026-05-10T18:56:57.136933Z","shell.execute_reply":"2026-05-10T18:56:57.153768Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"visualize_keypoints(images[0], feature_dir)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:56:57.155903Z","iopub.execute_input":"2026-05-10T18:56:57.156256Z","iopub.status.idle":"2026-05-10T18:56:57.477108Z","shell.execute_reply.started":"2026-05-10T18:56:57.156220Z","shell.execute_reply":"2026-05-10T18:56:57.476145Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Try first valid pair\nwith h5py.File(f'{feature_dir}/matches.h5','r') as f:\n    for k1 in f.keys():\n        for k2 in f[k1].keys():\n            img1 = os.path.join(images_dir, k1)\n            img2 = os.path.join(images_dir, k2)\n            visualize_matches(img1, img2, feature_dir)\n            break\n        break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:56:57.479498Z","iopub.execute_input":"2026-05-10T18:56:57.479762Z","iopub.status.idle":"2026-05-10T18:56:57.838417Z","shell.execute_reply.started":"2026-05-10T18:56:57.479739Z","shell.execute_reply":"2026-05-10T18:56:57.837503Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_match_histogram(feature_dir)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:56:57.839522Z","iopub.execute_input":"2026-05-10T18:56:57.839759Z","iopub.status.idle":"2026-05-10T18:56:58.091072Z","shell.execute_reply.started":"2026-05-10T18:56:57.839739Z","shell.execute_reply":"2026-05-10T18:56:58.090177Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_reconstruction_summary(maps)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:56:58.091968Z","iopub.execute_input":"2026-05-10T18:56:58.092315Z","iopub.status.idle":"2026-05-10T18:56:58.303657Z","shell.execute_reply.started":"2026-05-10T18:56:58.092247Z","shell.execute_reply":"2026-05-10T18:56:58.302505Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport h5py\n\nfrom sklearn.linear_model import RANSACRegressor\n\n# =========================\n# RANSAC FILTER (GEOMETRIC CONSISTENCY)\n# =========================\ndef compute_inliers_ransac(kp1, kp2, matches):\n    if len(matches) < 8:\n        return np.zeros(len(matches), dtype=bool)\n\n    pts1 = kp1[matches[:,0]]\n    pts2 = kp2[matches[:,1]]\n\n    # Fit simple affine model as proxy (fast + stable)\n    ransac = RANSACRegressor(residual_threshold=5.0)\n    ransac.fit(pts1, pts2[:,0])  # approximate projection\n\n    inliers = ransac.inlier_mask_\n    return inliers\n\n\n# =========================\n# MAIN VISUALIZER\n# =========================\ndef visualize_raw_vs_inliers(img1_path, img2_path, feature_dir, max_matches=80):\n\n    # load images\n    img1 = cv2.imread(img1_path)\n    img2 = cv2.imread(img2_path)\n    img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)\n    img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)\n\n    k1 = img1_path.split('/')[-1]\n    k2 = img2_path.split('/')[-1]\n\n    # load features + matches\n    with h5py.File(f'{feature_dir}/keypoints.h5','r') as f_kp, \\\n         h5py.File(f'{feature_dir}/matches.h5','r') as f_m:\n\n        if k1 not in f_m or k2 not in f_m[k1]:\n            print(\"No matches found\")\n            return\n\n        kp1 = f_kp[k1][...]\n        kp2 = f_kp[k2][...]\n        matches = f_m[k1][k2][...]\n\n    matches = matches[:max_matches]\n\n    # compute inliers\n    inliers = compute_inliers_ransac(kp1, kp2, matches)\n\n    # canvas setup\n    h1, w1 = img1.shape[:2]\n    h2, w2 = img2.shape[:2]\n\n    canvas_raw = np.zeros((max(h1,h2), w1+w2, 3), dtype=np.uint8)\n    canvas_inl = np.zeros_like(canvas_raw)\n\n    canvas_raw[:h1,:w1] = img1\n    canvas_raw[:h2,w1:] = img2\n\n    canvas_inl[:h1,:w1] = img1\n    canvas_inl[:h2,w1:] = img2\n\n    # =========================\n    # DRAW MATCHES\n    # =========================\n    fig, axs = plt.subplots(1,2, figsize=(16,7))\n\n    axs[0].imshow(canvas_raw)\n    axs[0].set_title(f\"RAW MATCHES ({len(matches)})\")\n    axs[0].axis('off')\n\n    axs[1].imshow(canvas_inl)\n    axs[1].set_title(f\"RANSAC INLIERS ({np.sum(inliers)})\")\n    axs[1].axis('off')\n\n    # draw lines\n    for i, (m, inl) in enumerate(zip(matches, inliers)):\n        x1, y1 = kp1[m[0]]\n        x2, y2 = kp2[m[1]]\n\n        color = 'lime' if inl else 'red'\n        alpha = 1.0 if inl else 0.2\n\n        axs[0].plot([x1, x2+w1], [y1, y2], color=color, alpha=alpha, linewidth=0.8)\n        axs[1].plot([x1, x2+w1], [y1, y2], color='lime', alpha=0.9, linewidth=1.0)\n\n    plt.tight_layout()\n    plt.show()\n\n    # =========================\n    # STATS\n    # =========================\n    print(\"Total matches:\", len(matches))\n    print(\"Inliers:\", np.sum(inliers))\n    print(\"Inlier ratio:\", np.mean(inliers))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:56:58.304789Z","iopub.execute_input":"2026-05-10T18:56:58.305186Z","iopub.status.idle":"2026-05-10T18:56:58.318642Z","shell.execute_reply.started":"2026-05-10T18:56:58.305149Z","shell.execute_reply":"2026-05-10T18:56:58.317681Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\ndataset = list(samples.keys())[0]\nfeature_dir = os.path.join(workdir, 'featureout', dataset)\nimages_dir = os.path.join(data_dir, 'test', dataset)\n\nimages = [os.path.join(images_dir, p.filename) for p in samples[dataset]]\n\n# ✅ RECREATE PAIRS HERE\npairs = get_image_pairs_shortlist(\n    images,\n    sim_th=0.25,\n    min_pairs=15,\n    max_pairs_per_img=50,\n    device=device\n)\n\nprint(\"Number of pairs:\", len(pairs))\n\n# safety check\nif len(pairs) == 0:\n    raise ValueError(\"No pairs found. Check feature extraction or sim_th.\")\n\ni, j = pairs[0]\n\nvisualize_raw_vs_inliers(images[i], images[j], feature_dir)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:58:03.881422Z","iopub.execute_input":"2026-05-10T18:58:03.881792Z","iopub.status.idle":"2026-05-10T18:58:04.848165Z","shell.execute_reply.started":"2026-05-10T18:58:03.881766Z","shell.execute_reply":"2026-05-10T18:58:04.847146Z"}},"outputs":[],"execution_count":null}]}