{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.10.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":115267,"databundleVersionId":13761094,"isSourceIdPinned":false},{"sourceType":"datasetVersion","sourceId":7884485,"datasetId":4628051,"databundleVersionId":7990559},{"sourceType":"datasetVersion","sourceId":11924468,"datasetId":6988459,"databundleVersionId":12432458},{"sourceType":"modelInstanceVersion","sourceId":17578,"databundleVersionId":7981336,"modelInstanceId":14634,"modelId":22086,"isSourceIdPinned":false},{"sourceType":"modelInstanceVersion","sourceId":17579,"databundleVersionId":7981340,"modelInstanceId":14635,"modelId":22086,"isSourceIdPinned":false},{"sourceType":"modelInstanceVersion","sourceId":17555,"databundleVersionId":7980546,"modelInstanceId":14611,"modelId":22086,"isSourceIdPinned":false},{"sourceType":"modelInstanceVersion","sourceId":4534,"databundleVersionId":6346558,"modelInstanceId":3326,"modelId":986,"isSourceIdPinned":false},{"sourceType":"modelInstanceVersion","sourceId":4535,"databundleVersionId":6346563,"modelInstanceId":3327,"modelId":986,"isSourceIdPinned":false},{"sourceType":"modelInstanceVersion","sourceId":4537,"databundleVersionId":6346609,"modelInstanceId":3329,"modelId":986,"isSourceIdPinned":false},{"sourceType":"modelInstanceVersion","sourceId":17191,"databundleVersionId":7971915,"modelInstanceId":14317,"modelId":21716,"isSourceIdPinned":false},{"sourceType":"kernelVersion","sourceId":234271505,"isSourceIdPinned":false},{"sourceType":"kernelVersion","sourceId":237741314,"isSourceIdPinned":false}],"dockerImageVersionId":30919,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":16027.297615,"end_time":"2026-05-02T01:05:12.005817","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-05-01T20:38:04.708202","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"30290963","cell_type":"markdown","source":"# Dependencies","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","papermill":{"duration":0.008206,"end_time":"2026-05-01T20:38:07.251732","exception":false,"start_time":"2026-05-01T20:38:07.243526","status":"completed"},"tags":[]}},{"id":"bb41e740","cell_type":"code","source":"# install colmap(CPU)\n!cd /kaggle/input/pkg-colmap/colmap_offline && dpkg -i ./*.deb\n\n# test\n!colmap -h","metadata":{"_kg_hide-output":true,"execution":{"iopub.execute_input":"2026-05-01T20:38:07.267452Z","iopub.status.busy":"2026-05-01T20:38:07.267180Z","iopub.status.idle":"2026-05-01T20:38:15.252086Z","shell.execute_reply":"2026-05-01T20:38:15.251037Z"},"papermill":{"duration":7.994578,"end_time":"2026-05-01T20:38:15.253705","exception":false,"start_time":"2026-05-01T20:38:07.259127","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"5a38351c","cell_type":"code","source":"# IMPORTANT \n#Install dependencies and copy model weights to run the notebook without internet access when submitting to the competition.\n\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/aliked/pytorch/aliked-n32/1/aliked-n32.pth /root/.cache/torch/hub/checkpoints/\n!cp /kaggle/input/aliked/pytorch/aliked-n16rot/1/aliked-n16rot.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":{"execution":{"iopub.execute_input":"2026-05-01T20:38:15.277815Z","iopub.status.busy":"2026-05-01T20:38:15.277554Z","iopub.status.idle":"2026-05-01T20:38:21.090985Z","shell.execute_reply":"2026-05-01T20:38:21.089712Z"},"papermill":{"duration":5.827171,"end_time":"2026-05-01T20:38:21.092789","exception":false,"start_time":"2026-05-01T20:38:15.265618","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"15187825","cell_type":"code","source":"import sys\nimport os, glob\nfrom tqdm import tqdm\nfrom fastprogress import progress_bar\nfrom time import time, sleep\nimport gc\nimport numpy as np\nimport h5py\nimport warnings\nimport dataclasses\nimport pandas as pd\nfrom IPython.display import clear_output\nfrom collections import defaultdict\nfrom copy import deepcopy\nfrom PIL import Image\nimport networkx as nx\n\nimport cv2\nimport torch\nimport torch.nn.functional as F\nimport kornia as K\nimport kornia.feature as KF\n\nimport torch\nfrom lightglue import match_pair\nfrom lightglue import ALIKED, LightGlue\nfrom lightglue.utils import load_image, rbd\nfrom transformers import AutoImageProcessor, AutoModel\n\n# IMPORTANT Utilities: importing data into colmap and competition metric\nimport pycolmap\nsys.path.append('/kaggle/input/imc25-utils')\nfrom database import *\nfrom h5_to_db import *\nimport metric\n# Do not forget to select an accelerator on the sidebar to the right.\n#device = K.utils.get_cuda_device_if_available(0)\n#print(f'{device=}')\n\nimport concurrent.futures\nimport traceback","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:21.117750Z","iopub.status.busy":"2026-05-01T20:38:21.117499Z","iopub.status.idle":"2026-05-01T20:38:43.673696Z","shell.execute_reply":"2026-05-01T20:38:43.672918Z"},"papermill":{"duration":22.569962,"end_time":"2026-05-01T20:38:43.675384","exception":false,"start_time":"2026-05-01T20:38:21.105422","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"970824f8","cell_type":"markdown","source":"# Configurations\n\nThis version restructures the notebook into a **cluster-first hierarchical sparse pipeline**:\n\n1. **DINOv2 retrieval**\n2. **Early scene clustering from the retrieval graph**\n3. **Cheap ALIKED local verification**\n4. **Single main ALIKED + LightGlue pass**\n5. **Graph pruning + cluster-wise COLMAP**\n6. **Targeted recovery pass only for weak / unregistered images**\n\nThe goal is to make the method visibly different from a parameter-tuned 11th-place style notebook while staying feasible to run.\n","metadata":{"papermill":{"duration":0.011543,"end_time":"2026-05-01T20:38:43.699641","exception":false,"start_time":"2026-05-01T20:38:43.688098","status":"completed"},"tags":[]}},{"id":"f9535517","cell_type":"code","source":"gpu_count = torch.cuda.device_count()\nif gpu_count == 0:\n    raise RuntimeError('CUDA GPU is required for this notebook.')\n\ndevice0 = torch.device('cuda:0')\ndevice1 = torch.device('cuda:1') if gpu_count > 1 else torch.device('cuda:0')\n\ndef switch_gpu(device, device_index):\n    if device is None:\n        return device0, 0\n    elif device == device0:\n        return device1, 1 if gpu_count > 1 else 0\n    else:\n        return device0, 0\n","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:43.724774Z","iopub.status.busy":"2026-05-01T20:38:43.724231Z","iopub.status.idle":"2026-05-01T20:38:43.764097Z","shell.execute_reply":"2026-05-01T20:38:43.763368Z"},"papermill":{"duration":0.054007,"end_time":"2026-05-01T20:38:43.765558","exception":false,"start_time":"2026-05-01T20:38:43.711551","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"62564f91","cell_type":"code","source":"\nclass Param_AlikedLightGlue:\n    def __init__(\n        self,\n        min_matches = 15,\n        max_num_keypoints = 4096,\n        image_size = 1024,\n        use_rotation = False,\n    ):\n        self.min_matches = min_matches\n        self.max_num_keypoints = max_num_keypoints\n        self.image_size = image_size\n        self.use_rotation = use_rotation\n\n\nclass CONFIG:\n    # ----------------------------\n    # Stage 1: global retrieval\n    # ----------------------------\n    exhaustive_if_less = 20\n    global_topk = 40\n    retrieval_keep_per_image = 20\n    cluster_similarity_threshold = 0.4\n    min_cluster_size = 2\n\n    # ----------------------------\n    # Stage 2: cheap local verifier\n    # ----------------------------\n    verifier_resize = 1024\n    verifier_keypoints = 2048\n    verifier_min_matches = 20\n    verifier_geom_check = True\n    verifier_ransac_threshold = 1.5\n    verifier_top_edges_per_image = 8\n\n    # ----------------------------\n    # Stage 3: main sparse matcher\n    # ----------------------------\n    main_match = Param_AlikedLightGlue(\n        min_matches = 18,\n        max_num_keypoints = 6144,\n        image_size = 2048,\n        use_rotation = False,\n    )\n\n    # ----------------------------\n    # Stage 4: targeted recovery\n    # ----------------------------\n    enable_recovery = True\n    recovery_expand_topk = 16\n    recovery_match = Param_AlikedLightGlue(\n        min_matches = 15,\n        max_num_keypoints = 4096,\n        image_size = 1024,\n        use_rotation = False,\n    )\n\n    # ----------------------------\n    # SfM\n    # ----------------------------\n    num_pallalel_sfm = 4\n    merge_top_edges_per_image = 12\n","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:43.790257Z","iopub.status.busy":"2026-05-01T20:38:43.790030Z","iopub.status.idle":"2026-05-01T20:38:43.795349Z","shell.execute_reply":"2026-05-01T20:38:43.794720Z"},"papermill":{"duration":0.018739,"end_time":"2026-05-01T20:38:43.796426","exception":false,"start_time":"2026-05-01T20:38:43.777687","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"66044ea8","cell_type":"markdown","source":"\n## Visible changes vs previous notebook\n\n### What changed\n- **Clustering moved to the front**: the old notebook matched first and split later; the new one builds scene clusters from DINO retrieval **before** expensive matching.\n- **Two-stage pair selection**: the old notebook relied on DINO shortlist + broad ALIKED matching settings; the new one uses a **cheap ALIKED verifier** to keep only promising pairs.\n- **Single main matcher instead of multiple broad passes**: the old notebook ran **two ALIKED configurations** and rotation checks broadly; the new notebook uses **one main ALIKED + LightGlue pass**.\n- **Recovery is targeted**: rotation and lighter settings are now used only for **unregistered / weak images**, not for every pair.\n- **Cleaner graph pruning**: before COLMAP, only the strongest verified edges are kept, reducing noisy cross-scene links.\n\n### Why these changes\n- To make the method **architecturally different**, not just parameter-tuned.\n- To reduce wasted compute on bad pairs.\n- To fit IMC 2025's messy multi-scene setting better.\n- To make the notebook easier to explain as **your own pipeline**.\n","metadata":{"papermill":{"duration":0.011328,"end_time":"2026-05-01T20:38:43.819929","exception":false,"start_time":"2026-05-01T20:38:43.808601","status":"completed"},"tags":[]}},{"id":"68c58310","cell_type":"markdown","source":"# Colmap Utilities","metadata":{"papermill":{"duration":0.011453,"end_time":"2026-05-01T20:38:43.842869","exception":false,"start_time":"2026-05-01T20:38:43.831416","status":"completed"},"tags":[]}},{"id":"bfe5ab69","cell_type":"code","source":"def import_into_colmap(img_dir, feature_dir ='.featureout', database_path = 'colmap.db'):\n    db = COLMAPDatabase.connect(database_path)\n    db.create_tables()\n    single_camera = False\n    fname_to_id = add_keypoints(db, feature_dir, img_dir, '', 'simple-radial', single_camera)\n    add_matches(\n        db,\n        feature_dir,\n        fname_to_id,\n    )\n    db.commit()\n    return","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:43.867258Z","iopub.status.busy":"2026-05-01T20:38:43.867031Z","iopub.status.idle":"2026-05-01T20:38:43.870823Z","shell.execute_reply":"2026-05-01T20:38:43.870182Z"},"papermill":{"duration":0.017331,"end_time":"2026-05-01T20:38:43.872007","exception":false,"start_time":"2026-05-01T20:38:43.854676","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"6acbe0a4","cell_type":"code","source":"def add_keypoints_with_scene(db, scene, h5_path, image_path, img_ext, camera_model, single_camera = True):\n    keypoint_f = h5py.File(os.path.join(h5_path, 'keypoints.h5'), 'r')\n\n    camera_id = None\n    fname_to_id = {}\n    for filename in tqdm(list(keypoint_f.keys())):\n        if not filename in scene:\n            continue\n        \n        keypoints = keypoint_f[filename][()]\n\n        fname_with_ext = filename# + img_ext\n        path = os.path.join(image_path, fname_with_ext)\n        if not os.path.isfile(path):\n            raise IOError(f'Invalid image path {path}')\n\n        if camera_id is None or not single_camera:\n            camera_id = create_camera(db, path, camera_model)\n        image_id = db.add_image(fname_with_ext, camera_id)\n        fname_to_id[filename] = image_id\n\n        db.add_keypoints(image_id, keypoints)\n\n    return fname_to_id\n\ndef add_matches_with_scene(db, scene, h5_path, fname_to_id):\n    match_file = h5py.File(os.path.join(h5_path, 'matches.h5'), 'r')\n    \n    added = set()\n    n_keys = len(match_file.keys())\n    n_total = (n_keys * (n_keys - 1)) // 2\n\n    with tqdm(total=n_total) as pbar:\n        for key_1 in match_file.keys():\n            if not key_1 in scene:\n                continue\n\n            group = match_file[key_1]\n            for key_2 in group.keys():\n                if not key_2 in scene:\n                    continue\n                    \n                id_1 = fname_to_id[key_1]\n                id_2 = fname_to_id[key_2]\n\n                pair_id = image_ids_to_pair_id(id_1, id_2)\n                if pair_id in added:\n                    warnings.warn(f'Pair {pair_id} ({id_1}, {id_2}) already added!')\n                    continue\n            \n                matches = group[key_2][()]\n                db.add_matches(id_1, id_2, matches)\n\n                added.add(pair_id)\n\n                pbar.update(1)\n\ndef import_into_colmap_with_scene(img_dir, scene, feature_dir ='.featureout', database_path = 'colmap.db'):\n    db = COLMAPDatabase.connect(database_path)\n    db.create_tables()\n    single_camera = False\n    fname_to_id = add_keypoints(db, feature_dir, img_dir, '', 'simple-radial', single_camera)\n    #fname_to_id = add_keypoints_with_scene(db, scene, feature_dir, img_dir, '', 'simple-radial', single_camera)\n    add_matches_with_scene(\n        db,\n        scene,\n        feature_dir,\n        fname_to_id,\n    )\n    db.commit()\n    return","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:43.895993Z","iopub.status.busy":"2026-05-01T20:38:43.895728Z","iopub.status.idle":"2026-05-01T20:38:43.903389Z","shell.execute_reply":"2026-05-01T20:38:43.902758Z"},"papermill":{"duration":0.020904,"end_time":"2026-05-01T20:38:43.904459","exception":false,"start_time":"2026-05-01T20:38:43.883555","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"56e88446","cell_type":"markdown","source":"# Pairs Reranker","metadata":{"papermill":{"duration":0.011779,"end_time":"2026-05-01T20:38:43.927867","exception":false,"start_time":"2026-05-01T20:38:43.916088","status":"completed"},"tags":[]}},{"id":"263d7cad","cell_type":"code","source":"\nimport os, torch, urllib.request, tempfile\nfrom PIL import Image\nfrom tqdm import tqdm\nfrom torchvision import transforms\nfrom collections import defaultdict\n\n\n# ---------- Global embeddings ---------- #\ndef _extract_dino_embeddings(fnames, device = torch.device('cuda')):\n    processor = AutoImageProcessor.from_pretrained('/kaggle/input/dinov2/pytorch/base/1')\n    model = AutoModel.from_pretrained('/kaggle/input/dinov2/pytorch/base/1')\n    model = model.eval().to(device)\n\n    global_descs_dinov2 = []\n    for img_fname_full in tqdm(fnames, total=len(fnames), desc=\"DINOv2\"):\n        timg = load_torch_image(img_fname_full)\n        with torch.inference_mode():\n            inputs = processor(images=timg, return_tensors=\"pt\", do_rescale=False).to(device)\n            outputs = model(**inputs)\n            dino_mac = F.normalize(outputs.last_hidden_state[:,1:].max(dim=1)[0], dim=1, p=2)\n        global_descs_dinov2.append(dino_mac.detach().cpu())\n    global_descs_dinov2 = torch.cat(global_descs_dinov2, dim=0)\n    return global_descs_dinov2\n\n\ndef _compute_similarity_matrix(global_feats, device):\n    g = global_feats.to(device)\n    sim = g @ g.T\n    sim.fill_diagonal_(0)\n    return sim.detach().cpu()\n\n\ndef _get_topk_neighbors(sim_matrix, k):\n    n = sim_matrix.shape[0]\n    if n <= 1:\n        return [[]]\n    k = min(k, n - 1)\n    vals, inds = torch.topk(sim_matrix, k=k, dim=1)\n    return inds.tolist(), vals.tolist()\n\n\n\ndef _guaranteed_retrieval_pairs(fnames, topk_neighbors, keep_per_image, scene_clusters=None):\n    \"\"\"\n    Keep top DINO retrieval pairs even if the cheap verifier rejects them.\n\n    Why:\n    - DINO finds visually similar image pairs.\n    - The cheap verifier can be too strict on hard/repetitive scenes like stairs.\n    - These guaranteed pairs are still checked later by the stronger main matcher.\n    \"\"\"\n    guaranteed_pairs = set()\n\n    image_to_cluster = {}\n    if scene_clusters is not None:\n        for cluster_id, cluster in enumerate(scene_clusters):\n            for image_name in cluster:\n                image_to_cluster[image_name] = cluster_id\n\n    for i in range(len(fnames)):\n        image_name = fnames[i]\n        neighbors = topk_neighbors[i]\n        selected_neighbors = neighbors[:keep_per_image]\n\n        for j in selected_neighbors:\n            if i == j:\n                continue\n\n            other_name = fnames[j]\n\n            # Safety for current notebook:\n            # keep guaranteed pairs inside the same early DINO scene cluster.\n            if scene_clusters is not None:\n                if image_name in image_to_cluster and other_name in image_to_cluster:\n                    if image_to_cluster[image_name] != image_to_cluster[other_name]:\n                        continue\n\n            guaranteed_pairs.add((min(i, j), max(i, j)))\n\n    return guaranteed_pairs\n\n\ndef _build_retrieval_graph(fnames, sim_matrix, topk_neighbors, similarity_threshold):\n    G = nx.Graph()\n    for fname in fnames:\n        G.add_node(fname)\n\n    for i, nbrs in enumerate(topk_neighbors):\n        for j in nbrs:\n            score = float(sim_matrix[i, j])\n            if score >= similarity_threshold:\n                G.add_edge(fnames[i], fnames[j], weight=score)\n\n    # fallback to a full component if the graph is too sparse\n    if G.number_of_edges() == 0:\n        for i in range(len(fnames)):\n            for j in range(i + 1, len(fnames)):\n                G.add_edge(fnames[i], fnames[j], weight=float(sim_matrix[i, j]))\n\n    return G\n\n\ndef _clusters_from_retrieval_graph(G, min_cluster_size=3):\n    components = [sorted(list(c)) for c in nx.connected_components(G)]\n    strong_clusters = [c for c in components if len(c) >= min_cluster_size]\n    weak_images = []\n    for c in components:\n        if len(c) < min_cluster_size:\n            weak_images.extend(c)\n\n    if len(strong_clusters) == 0 and len(components) > 0:\n        strong_clusters = components\n\n    return strong_clusters, weak_images\n\n\n# ---------- ALIKED local features ---------- #\ndef _get_aliked_model(device, num_features, resize_to=1024, detection_threshold=0.01):\n    dtype = torch.float32\n    extractor = ALIKED(\n        model_name=\"aliked-n16\",\n        max_num_keypoints=num_features,\n        detection_threshold=detection_threshold,\n        resize=resize_to\n    ).eval().to(device, dtype)\n    extractor.preprocess_conf[\"resize\"] = resize_to\n    return extractor\n\n\n@torch.no_grad()\ndef _extract_local_features(img_paths, model, device):\n    dtype = torch.float32\n    local_feats = []\n    for p in tqdm(img_paths, desc=\"ALIKED verifier\"):\n        image0 = load_torch_image(p, device=device).to(dtype)\n        feats0 = model.extract(image0)\n        k = feats0['keypoints'].reshape(-1, 2).detach().cpu()\n        d = feats0['descriptors'].reshape(-1, 128).detach().cpu()\n        d = torch.nn.functional.normalize(d, dim=1)\n        local_feats.append({\"keypoints\": k, \"descriptors\": d})\n    return local_feats\n\n\n@torch.no_grad()\ndef _mutual_nn_matches(desc1, desc2, device):\n    if desc1.size(0) == 0 or desc2.size(0) == 0:\n        return 0, None, None\n\n    dist = torch.cdist(desc1.to(device), desc2.to(device), p=2)\n    nn12 = torch.argmin(dist, dim=1)\n    nn21 = torch.argmin(dist, dim=0)\n\n    idx1 = torch.arange(desc1.size(0), device=device)\n    mutual = nn21[nn12] == idx1\n    idx2 = nn12[mutual]\n    idx1 = idx1[mutual]\n\n    return int(mutual.sum().item()), idx1.cpu(), idx2.cpu()\n\n\ndef _quick_geometric_check(kpts1, kpts2, idx1, idx2, threshold=1.5):\n    if idx1 is None or idx2 is None or len(idx1) < 8:\n        return 0\n\n    pts1 = kpts1[idx1.numpy()]\n    pts2 = kpts2[idx2.numpy()]\n\n    try:\n        Fmat, inliers = cv2.findFundamentalMat(\n            pts1.astype(np.float32),\n            pts2.astype(np.float32),\n            method=cv2.USAC_MAGSAC,\n            ransacReprojThreshold=threshold,\n            confidence=0.999,\n            maxIters=1000,\n        )\n        if inliers is None:\n            return 0\n        return int(inliers.sum())\n    except:\n        return 0\n\n\ndef _build_verified_pairs(fnames, cluster_images, image_to_idx, topk_neighbors, local_feats, device):\n    edge_scores = []\n    for img_name in tqdm(cluster_images, desc=\"Local pair verification\"):\n        i = image_to_idx[img_name]\n        for j in topk_neighbors[i]:\n            other_name = fnames[j]\n            if other_name not in cluster_images or i >= j:\n                continue\n\n            mutual_count, idx1, idx2 = _mutual_nn_matches(\n                local_feats[i][\"descriptors\"],\n                local_feats[j][\"descriptors\"],\n                device\n            )\n\n            if mutual_count < CONFIG.verifier_min_matches:\n                continue\n\n            score = mutual_count\n            if CONFIG.verifier_geom_check:\n                inlier_count = _quick_geometric_check(\n                    local_feats[i][\"keypoints\"].numpy(),\n                    local_feats[j][\"keypoints\"].numpy(),\n                    idx1,\n                    idx2,\n                    threshold=CONFIG.verifier_ransac_threshold,\n                )\n                if inlier_count < 8:\n                    continue\n                score = inlier_count\n\n            edge_scores.append((i, j, score))\n\n    # keep only strongest edges per image\n    keep = []\n    per_image = defaultdict(list)\n    for i, j, score in edge_scores:\n        per_image[i].append((score, i, j))\n        per_image[j].append((score, i, j))\n\n    selected = set()\n    for _, entries in per_image.items():\n        entries = sorted(entries, reverse=True)[:CONFIG.verifier_top_edges_per_image]\n        for score, i, j in entries:\n            selected.add((min(i, j), max(i, j)))\n\n    keep = sorted(list(selected))\n    return keep\n\n\ndef build_clustered_verified_pairs(fnames, device=torch.device(\"cuda\")):\n    assert len(fnames) > 1, \"fnames must contain at least two images\"\n\n    # 1) DINO retrieval\n    global_feats = _extract_dino_embeddings(fnames, device)\n    sim_matrix = _compute_similarity_matrix(global_feats, device)\n    topk_neighbors, _ = _get_topk_neighbors(sim_matrix, CONFIG.global_topk)\n\n    # 2) Early clustering from retrieval graph\n    retrieval_graph = _build_retrieval_graph(\n        fnames,\n        sim_matrix,\n        topk_neighbors,\n        CONFIG.cluster_similarity_threshold,\n    )\n    scene_clusters, weak_images = _clusters_from_retrieval_graph(\n        retrieval_graph,\n        min_cluster_size=CONFIG.min_cluster_size,\n    )\n\n    # 3) Cheap ALIKED verification\n    verifier = _get_aliked_model(\n        device,\n        CONFIG.verifier_keypoints,\n        resize_to=CONFIG.verifier_resize,\n    )\n    local_feats = _extract_local_features(fnames, verifier, device)\n\n    image_to_idx = {fname: i for i, fname in enumerate(fnames)}\n    verified_pairs = []\n    pair_scores = {}\n    for cluster in scene_clusters:\n        cluster_set = set(cluster)\n        cluster_pairs = _build_verified_pairs(\n            fnames, cluster_set, image_to_idx, topk_neighbors, local_feats, device\n        )\n        for i, j in cluster_pairs:\n            verified_pairs.append((i, j))\n            pair_scores[(i, j)] = pair_scores.get((i, j), 0) + 1\n\n    # Keep the verifier-approved pairs.\n    verified_pairs_before_retrieval = sorted(list(set(verified_pairs)))\n\n    # NEW CHANGE:\n    # Also keep the top DINO retrieval pairs for every image.\n    # This gives difficult pairs a chance to reach the stronger main matcher.\n    retrieval_pairs = _guaranteed_retrieval_pairs(\n        fnames,\n        topk_neighbors,\n        CONFIG.retrieval_keep_per_image,\n        scene_clusters=scene_clusters,\n    )\n\n    verified_pairs = sorted(list(set(verified_pairs_before_retrieval) | retrieval_pairs))\n\n    print(\"Cheap verifier pairs:\", len(verified_pairs_before_retrieval))\n    print(\"Guaranteed DINO retrieval pairs:\", len(retrieval_pairs))\n    print(\"Final pairs sent to main matcher:\", len(verified_pairs))\n\n    del verifier\n    torch.cuda.empty_cache()\n    gc.collect()\n\n    return {\n        \"verified_pairs\": verified_pairs,\n        \"scene_clusters\": scene_clusters,\n        \"weak_images\": weak_images,\n        \"retrieval_graph\": retrieval_graph,\n        \"similarity_matrix\": sim_matrix,\n        \"topk_neighbors\": topk_neighbors,\n    }\n\n\ndef get_img_pairs_exhaustive(img_fnames):\n    index_pairs = []\n    for i in range(len(img_fnames)):\n        for j in range(i+1, len(img_fnames)):\n            index_pairs.append((i,j))\n    return index_pairs\n\n\ndef get_image_pairs_shortlist(fnames, device=torch.device('cuda')):\n    if len(fnames) <= CONFIG.exhaustive_if_less:\n        return {\n            \"verified_pairs\": get_img_pairs_exhaustive(fnames),\n            \"scene_clusters\": [sorted(fnames)],\n            \"weak_images\": [],\n            \"retrieval_graph\": None,\n            \"similarity_matrix\": None,\n            \"topk_neighbors\": None,\n        }\n    return build_clustered_verified_pairs(fnames, device)\n","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:43.951630Z","iopub.status.busy":"2026-05-01T20:38:43.951430Z","iopub.status.idle":"2026-05-01T20:38:43.977754Z","shell.execute_reply":"2026-05-01T20:38:43.977125Z"},"papermill":{"duration":0.039785,"end_time":"2026-05-01T20:38:43.979015","exception":false,"start_time":"2026-05-01T20:38:43.939230","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"3178ec8d","cell_type":"markdown","source":"# Image pairs","metadata":{"papermill":{"duration":0.011279,"end_time":"2026-05-01T20:38:44.001954","exception":false,"start_time":"2026-05-01T20:38:43.990675","status":"completed"},"tags":[]}},{"id":"f57ce813","cell_type":"code","source":"def load_torch_image(fname, device=torch.device('cuda')):\n    img = K.io.load_image(fname, K.io.ImageLoadType.RGB32, device=device)[None, ...]\n    return img\n\n# Must Use efficientnet global descriptor to get matching shortlists.\ndef get_global_desc(fnames, device = torch.device('cuda')):\n    processor = AutoImageProcessor.from_pretrained('/kaggle/input/dinov2/pytorch/base/1')\n    model = AutoModel.from_pretrained('/kaggle/input/dinov2/pytorch/base/1')\n    model = model.eval()\n    model = model.to(device)\n    global_descs_dinov2 = []\n    for i, img_fname_full in tqdm(enumerate(fnames),total= len(fnames)):\n        key = os.path.splitext(os.path.basename(img_fname_full))[0]\n        timg = load_torch_image(img_fname_full)\n        with torch.inference_mode():\n            inputs = processor(images=timg, return_tensors=\"pt\", do_rescale=False).to(device)\n            outputs = model(**inputs)\n            dino_mac = F.normalize(outputs.last_hidden_state[:,1:].max(dim=1)[0], dim=1, p=2)\n        global_descs_dinov2.append(dino_mac.detach().cpu())\n    global_descs_dinov2 = torch.cat(global_descs_dinov2, dim=0)\n    return global_descs_dinov2\n\ndef get_img_pairs_exhaustive(img_fnames):\n    index_pairs = []\n    for i in range(len(img_fnames)):\n        for j in range(i+1, len(img_fnames)):\n            index_pairs.append((i,j))\n    return index_pairs\n","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:44.025965Z","iopub.status.busy":"2026-05-01T20:38:44.025736Z","iopub.status.idle":"2026-05-01T20:38:44.031655Z","shell.execute_reply":"2026-05-01T20:38:44.031039Z"},"papermill":{"duration":0.019055,"end_time":"2026-05-01T20:38:44.032743","exception":false,"start_time":"2026-05-01T20:38:44.013688","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"79f88fa9","cell_type":"markdown","source":"# Aliked+LightGlue","metadata":{"papermill":{"duration":0.011121,"end_time":"2026-05-01T20:38:44.055353","exception":false,"start_time":"2026-05-01T20:38:44.044232","status":"completed"},"tags":[]}},{"id":"c971b4c2","cell_type":"code","source":"def convert_coord(r, w, h, rotk):\n    if rotk == 0:\n        return r\n    elif rotk == 1:\n        rx = w-1-r[:, 1]\n        ry = r[:, 0]\n        return torch.concat([rx[None], ry[None]], dim=0).T # np.array([rx, ry]).T\n    elif rotk == 2:\n        rx = w-1-r[:, 0]\n        ry = h-1-r[:, 1]\n        return torch.concat([rx[None], ry[None]], dim=0).T # np.array([rx, ry]).T\n    elif rotk == 3:\n        rx = r[:, 1]\n        ry = h-1-r[:, 0]\n        return torch.concat([rx[None], ry[None]], dim=0).T # np.array([rx, ry]).T\n\ndef matching_aliked_lightglue_rot(\n    img_fnames,\n    index_pairs,\n    rot,\n    saved_file,\n    feature_dir = '.featureout',\n    num_features = 4096,\n    resize_to = 1024,\n    device=torch.device('cuda'),\n    min_matches=15,\n    verbose=True,\n):\n    if not os.path.isdir(feature_dir):\n        os.makedirs(feature_dir)\n\n    #####################################################\n    # Extract keypoints and descriptions\n    #####################################################\n    dtype = torch.float32 # ALIKED has issues with float16\n    extractor = ALIKED(\n        model_name=\"aliked-n16\",\n        max_num_keypoints=num_features,\n        detection_threshold=0.01, #0.001, \n        resize=resize_to\n    ).eval().to(device, dtype)\n    print(\"aliked> image size =\", extractor.preprocess_conf[\"resize\"], \"-->\", resize_to )\n    extractor.preprocess_conf[\"resize\"] = resize_to\n    if not os.path.isdir(feature_dir):\n        os.makedirs(feature_dir)\n    \n    dict_kpts_cuda = {}\n    dict_descs_cuda = {}\n    for img_path in img_fnames:\n        img_fname = img_path.split('/')[-1]\n        key = img_fname\n\n        with torch.inference_mode():\n            rot_k = 0\n            image0 = load_torch_image(img_path, device=device).to(dtype)\n            h, w = image0.shape[2], image0.shape[3]\n            feats0 = extractor.extract(image0)  # auto-resize the image, disable with resize=None\n            kpts = feats0['keypoints'].reshape(-1, 2).detach()\n            descs = feats0['descriptors'].reshape(len(kpts), -1).detach()\n            dict_kpts_cuda[f\"{key}_{rot_k}\"] = kpts\n            dict_descs_cuda[f\"{key}_{rot_k}\"] = descs\n            if verbose:\n                print(f\"aliked_rot> rot_k={rot_k}, kpts.shape={kpts.shape}, descs.shape={descs.shape}\")\n\n        if rot != 0:\n            with torch.inference_mode():\n                rot_k = rot\n                image0 = load_torch_image(img_path, device=device).to(dtype)\n                h, w = image0.shape[2], image0.shape[3]\n                image1 = torch.rot90(image0, rot, [2, 3])\n                feats0 = extractor.extract(image1)  # auto-resize the image, disable with resize=None\n                kpts = feats0['keypoints'].reshape(-1, 2).detach()\n                descs = feats0['descriptors'].reshape(len(kpts), -1).detach()\n                kpts = convert_coord(kpts, w, h, rot_k)\n                dict_kpts_cuda[f\"{key}_{rot_k}\"] = kpts\n                dict_descs_cuda[f\"{key}_{rot_k}\"] = descs\n                if verbose:\n                    print(f\"aliked_rot> rot_k={rot_k}, kpts.shape={kpts.shape}, descs.shape={descs.shape}\")\n    del extractor\n    gc.collect()\n\n    #####################################################\n    # Matching keypoints\n    #####################################################\n    lg_matcher = KF.LightGlueMatcher(\n        \"aliked\", {\n            \"width_confidence\": -1,\n            \"depth_confidence\": -1,\n            #\"filter_threshold\": 0.5,\n            \"mp\": True if 'cuda' in str(device) else False\n        }\n    ).eval().to(device).half()\n    \n    cnt_pairs = 0\n    with h5py.File(saved_file, mode='w') as f_match:\n        for pair_idx in tqdm(index_pairs):\n            idx1, idx2 = pair_idx\n            fname1, fname2 = img_fnames[idx1], img_fnames[idx2]\n            if (\"outliers\" in fname1) or ( \"outliers\" in fname2 ):\n                continue\n            key1, key2 = fname1.split('/')[-1], fname2.split('/')[-1]\n            \n            kp1 = dict_kpts_cuda[f\"{key1}_0\"]\n            desc1 = dict_descs_cuda[f\"{key1}_0\"]\n                        \n            kp2 = dict_kpts_cuda[f\"{key2}_{rot}\"]\n            desc2 = dict_descs_cuda[f\"{key2}_{rot}\"]\n            with torch.inference_mode():\n                dists, idxs = lg_matcher(desc1.half(),\n                                     desc2.half(),\n                                     KF.laf_from_center_scale_ori(kp1.half()[None]),\n                                     KF.laf_from_center_scale_ori(kp2.half()[None]))\n            #print( f\"dists.shape={dists.shape}\")\n            #print( f\"type(dists)={type(dists)}\")\n            #print( f\"dists.mean()={dists.mean()}, {dists.max()}, {dists.min()}\")\n            if len(idxs)  == 0:\n                continue\n            kp1 = kp1[idxs[:,0], :].cpu().numpy().reshape(-1, 2).astype(np.float32)\n            kp2 = kp2[idxs[:,1], :].cpu().numpy().reshape(-1, 2).astype(np.float32)\n            confs = (1.0-dists).cpu().numpy().reshape(-1, 1).astype(np.float32)\n            n_matches = kp1.shape[0]\n            group  = f_match.require_group(key1)\n            if n_matches >= min_matches:\n                matches = np.concatenate([kp1, kp2, confs], axis=1)\n                #print(f\"@@@@ matches.shape = {matches.shape}\")\n                group.create_dataset(key2, data=matches)\n                cnt_pairs+=1\n                if verbose:\n                    print (f'aliked_rot> {key1}-{key2}@rot={rot}: {n_matches} matches @ {cnt_pairs}th pair')            \n            else:\n                if verbose:\n                    print (f'aliked_rot> {key1}-{key2}: {n_matches} matches --> skipped')\n    del lg_matcher\n    torch.cuda.empty_cache()\n    gc.collect()\n    return\n\n","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:44.078840Z","iopub.status.busy":"2026-05-01T20:38:44.078621Z","iopub.status.idle":"2026-05-01T20:38:44.092410Z","shell.execute_reply":"2026-05-01T20:38:44.091870Z"},"papermill":{"duration":0.026995,"end_time":"2026-05-01T20:38:44.093628","exception":false,"start_time":"2026-05-01T20:38:44.066633","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"6f987cbc","cell_type":"markdown","source":"# Keypoints merger & Matching Filter","metadata":{"papermill":{"duration":0.011361,"end_time":"2026-05-01T20:38:44.116570","exception":false,"start_time":"2026-05-01T20:38:44.105209","status":"completed"},"tags":[]}},{"id":"fdf40508","cell_type":"code","source":"def get_unique_idxs(A, dim=0):\n    # https://stackoverflow.com/questions/72001505/how-to-get-unique-elements-and-their-firstly-appeared-indices-of-a-pytorch-tenso\n    unique, idx, counts = torch.unique(A, dim=dim, sorted=True, return_inverse=True, return_counts=True)\n    _, ind_sorted = torch.sort(idx, stable=True)\n    cum_sum = counts.cumsum(0)\n    cum_sum = torch.cat((torch.tensor([0],device=cum_sum.device), cum_sum[:-1]))\n    first_indices = ind_sorted[cum_sum]\n    return first_indices\n\ndef get_keypoint_from_h5(fp, key1, key2):\n    rc = -1\n    try:\n        kpts = np.array(fp[key1][key2])\n        rc = 0\n        return (rc, kpts)\n    except:\n        return (rc, None)\n\ndef get_keypoint_from_multi_h5(fps, key1, key2):\n    list_mkpts = []\n    for fp in fps:\n        rc, mkpts = get_keypoint_from_h5(fp, key1, key2)\n        if mkpts is not None:\n            list_mkpts.append(mkpts)\n    if len(list_mkpts) > 0:\n        for data in list_mkpts:\n            print(f\"--> \", data.shape)\n        list_mkpts = np.concatenate(list_mkpts, axis=0)\n    else:\n        list_mkpts = None\n    return list_mkpts\n\ndef matches_merger(\n    img_fnames,\n    index_pairs,\n    files_keypoints,\n    save_file,\n    feature_dir = 'featureout',\n    filter_FundamentalMatrix = False,\n    filter_iterations = 10,\n    filter_threshold = 8,\n    min_matches=15,\n):\n    print( files_keypoints )\n    # open h5 files\n    fps = [ h5py.File(file, mode=\"r\") for file in files_keypoints ]\n    \n    with h5py.File(save_file, mode='w') as f_match:\n        counter = 0\n        for pair_idx in progress_bar(index_pairs):\n            idx1, idx2 = pair_idx\n            fname1, fname2 = img_fnames[idx1], img_fnames[idx2]\n            key1, key2 = fname1.split('/')[-1], fname2.split('/')[-1]\n\n            # extract keypoints\n            mkpts = get_keypoint_from_multi_h5(fps, key1, key2)\n            if mkpts is None:\n                print(f\"skipped key1={key1}, key2={key2}\")\n                continue\n\n            ori_size = mkpts.shape[0]\n            if mkpts.shape[0] < min_matches:\n                continue\n            \n            if filter_FundamentalMatrix:\n                store_inliers = { idx:0 for idx in range(mkpts.shape[0]) }\n                idxs = np.array(range(mkpts.shape[0]))\n                for iter in range(filter_iterations):\n                    try:\n                        Fm, inliers = cv2.findFundamentalMat(\n                            mkpts[:,:2], mkpts[:,2:4], cv2.USAC_MAGSAC, 0.15, 0.9999, 20000)\n                        if Fm is not None:\n                            inliers = inliers > 0\n                            inlier_idxs = idxs[inliers[:, 0]]\n                            #print(inliers.shape, inlier_idxs[:5])\n                            for idx in inlier_idxs:\n                                store_inliers[idx] += 1\n                    except:\n                        print(f\"Failed to cv2.findFundamentalMat. mkpts.shape={mkpts.shape}\")\n                inliers = np.array([ count for (idx, count) in store_inliers.items() ]) >= filter_threshold\n                mkpts = mkpts[inliers]\n                if mkpts.shape[0] < 15:\n                    print(f\"skipped key1={key1}, key2={key2}: mkpts.shape={mkpts.shape} after filtered.\")\n                    continue\n                #print(f\"filter_FundamentalMatrix: {len(store_inliers)} matches --> {mkpts.shape[0]} matches\")\n            \n            \n            print (f'{key1}-{key2}: {ori_size} --> {mkpts.shape[0]} matches')            \n            # regist tmp file\n            group  = f_match.require_group(key1)\n            group.create_dataset(key2, data=mkpts[:, :4])\n            counter += 1\n    print( f\"Ensembled pairs : {counter} pairs\" )\n    for fp in fps:\n        fp.close()\n\ndef get_adjacent_edges_with_weights(G, node):\n    \"\"\"\n    指定されたノードに隣接するエッジとその重みをリストで返す関数\n    \n    :param G: networkxのグラフオブジェクト\n    :param node: 対象となるノード\n    :return: (隣接ノード, 重み)のタプルのリスト、重みの降順でソートされる\n    \"\"\"\n    adjacent_edges = [(neighbor, G[node][neighbor]['weight']) for neighbor in G.neighbors(node)]\n    return sorted(adjacent_edges, key=lambda x: x[1], reverse=True)\n\ndef filter_mkpts(\n    file_mkpts, \n    save_file,\n    th_num_pairs,\n):\n    sleep(10)\n    G = nx.Graph()\n    pairs = []\n    with h5py.File(save_file, mode='w') as f_match, h5py.File(file_mkpts, \"r\") as fp:\n        for key1 in fp.keys():\n            for key2 in fp[key1].keys():\n                mkpts = fp[key1][key2]\n                G.add_edge(key1, key2, weight=mkpts.shape[0])\n                pairs.append([key1, key2, mkpts])\n\n        for (key1, key2, mkpts) in pairs:\n            neibors1 = get_adjacent_edges_with_weights(G, key1)\n            neibors2 = get_adjacent_edges_with_weights(G, key2)\n            if (key2 in [ val[0] for val in neibors1[:th_num_pairs] ]) or (key1 in [ val[0] for val in neibors2[:th_num_pairs] ]):\n                group  = f_match.require_group(key1)\n                group.create_dataset(key2, data=mkpts)\n\ndef keypoints_merger(\n    img_fnames,\n    index_pairs,\n    files_keypoints,\n    feature_dir = 'featureout',\n    filter_FundamentalMatrix = False,\n    filter_iterations = 10,\n    filter_threshold = 8,\n):\n    print(f\"files_keypoints = {files_keypoints}\")\n    save_file0 = f'{feature_dir}/merge_tmp0.h5'\n    !rm -rf {save_file0}\n    matches_merger(\n        img_fnames,\n        index_pairs,\n        files_keypoints,\n        save_file0,\n        feature_dir = feature_dir,\n        filter_FundamentalMatrix = filter_FundamentalMatrix,\n        filter_iterations = filter_iterations,\n        filter_threshold = filter_threshold,\n    )\n\n    th_num_pairs = CONFIG.merge_top_edges_per_image\n    save_file = f'{feature_dir}/merge_tmp.h5'\n    !rm -rf {save_file}\n    filter_mkpts(\n        save_file0, \n        save_file,\n        th_num_pairs,\n    )\n    \n    # Let's find unique loftr pixels and group them together.\n    kpts = defaultdict(list)\n    match_indexes = defaultdict(dict)\n    total_kpts=defaultdict(int)\n    with h5py.File(save_file, mode='r') as f_match:\n        for k1 in f_match.keys():\n            group  = f_match[k1]\n            for k2 in group.keys():\n                matches = group[k2][...]\n                total_kpts[k1]\n                kpts[k1].append(matches[:, :2])\n                kpts[k2].append(matches[:, 2:])\n                current_match = torch.arange(len(matches)).reshape(-1, 1).repeat(1, 2)\n                current_match[:, 0]+=total_kpts[k1]\n                current_match[:, 1]+=total_kpts[k2]\n                total_kpts[k1]+=len(matches)\n                total_kpts[k2]+=len(matches)\n                match_indexes[k1][k2]=current_match\n\n    for k in kpts.keys():\n        kpts[k] = np.round(np.concatenate(kpts[k], axis=0))\n    unique_kpts = {}\n    unique_match_idxs = {}\n    out_match = defaultdict(dict)\n    for k in kpts.keys():\n        uniq_kps, uniq_reverse_idxs = torch.unique(torch.from_numpy(kpts[k]),dim=0, return_inverse=True)\n        unique_match_idxs[k] = uniq_reverse_idxs\n        unique_kpts[k] = uniq_kps.numpy()\n    for k1, group in match_indexes.items():\n        for k2, m in group.items():\n            m2 = deepcopy(m)\n            m2[:,0] = unique_match_idxs[k1][m2[:,0]]\n            m2[:,1] = unique_match_idxs[k2][m2[:,1]]\n            mkpts = np.concatenate([unique_kpts[k1][ m2[:,0]],\n                                    unique_kpts[k2][  m2[:,1]],\n                                   ],\n                                   axis=1)\n            unique_idxs_current = get_unique_idxs(torch.from_numpy(mkpts), dim=0)\n            m2_semiclean = m2[unique_idxs_current]\n            unique_idxs_current1 = get_unique_idxs(m2_semiclean[:, 0], dim=0)\n            m2_semiclean = m2_semiclean[unique_idxs_current1]\n            unique_idxs_current2 = get_unique_idxs(m2_semiclean[:, 1], dim=0)\n            m2_semiclean2 = m2_semiclean[unique_idxs_current2]\n            out_match[k1][k2] = m2_semiclean2.numpy()\n    with h5py.File(f'{feature_dir}/keypoints.h5', mode='w') as f_kp:\n        for k, kpts1 in unique_kpts.items():\n            f_kp[k] = kpts1\n    \n    with h5py.File(f'{feature_dir}/matches.h5', mode='w') as f_match:\n        for k1, gr in out_match.items():\n            group  = f_match.require_group(k1)\n            for k2, match in gr.items():\n                group[k2] = match\n    return","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:44.140685Z","iopub.status.busy":"2026-05-01T20:38:44.140477Z","iopub.status.idle":"2026-05-01T20:38:44.182946Z","shell.execute_reply":"2026-05-01T20:38:44.182293Z"},"papermill":{"duration":0.055884,"end_time":"2026-05-01T20:38:44.184104","exception":false,"start_time":"2026-05-01T20:38:44.128220","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"c3bf4a01","cell_type":"code","source":"# def matching_roma(\n#     img_fnames,\n#     index_pairs,\n#     saved_file,\n#     feature_dir='.featureout',\n#     num_features=512,\n#     resize_to=560,\n#     device=torch.device('cuda'),\n#     min_matches=15,\n#     verbose=True,\n# ):\n#     import sys\n#     sys.path.append('/kaggle/input/pkg-roma/RoMa')\n#     from romatch import roma_outdoor\n\n#     if not os.path.isdir(feature_dir):\n#         os.makedirs(feature_dir)\n\n#     # Load RoMa model\n#     roma_device = torch.device('cpu')\n#     weights = torch.load('/kaggle/input/pkg-roma/roma_outdoor.pth', map_location=roma_device)\n#     dinov2_weights = torch.load('/kaggle/input/pkg-roma/dinov2_vitl14_pretrain.pth', map_location=roma_device)\n#     roma_model = roma_outdoor(\n#         device=roma_device,\n#         weights=weights,\n#         dinov2_weights=dinov2_weights,\n#         coarse_res=resize_to,\n#         upsample_res=560\n#     )\n#     roma_model.eval()    # ADD 1\n#     roma_model.float()   # ADD 2 — fixes c10::Half error\n\n#     with h5py.File(saved_file, mode='w') as f_match:\n#         for pair_idx in progress_bar(index_pairs):\n#             idx1, idx2 = pair_idx\n#             fname1, fname2 = img_fnames[idx1], img_fnames[idx2]\n#             key1 = fname1.split('/')[-1]\n#             key2 = fname2.split('/')[-1]\n#             try:\n#                 img1 = Image.open(fname1).convert('RGB')\n#                 img2 = Image.open(fname2).convert('RGB')\n#                 W1, H1 = img1.size\n#                 W2, H2 = img2.size\n#                 with torch.inference_mode():\n#                     warp, certainty = roma_model.match(fname1, fname2, device=roma_device)\n#                     matches, confidence = roma_model.sample(warp, certainty, num=num_features)\n#                 # Convert to pixel coords\n#                 kpts1 = matches[:, :2]\n#                 kpts2 = matches[:, 2:]\n#                 # Unnormalize\n#                 kpts1 = torch.stack([\n#                     (kpts1[:, 0] + 1) / 2 * W1,\n#                     (kpts1[:, 1] + 1) / 2 * H1,\n#                 ], dim=-1)\n#                 kpts2 = torch.stack([\n#                     (kpts2[:, 0] + 1) / 2 * W2,\n#                     (kpts2[:, 1] + 1) / 2 * H2,\n#                 ], dim=-1)\n#                 mkpts = torch.cat([kpts1, kpts2], dim=1).cpu().numpy()\n#                 if mkpts.shape[0] < min_matches:\n#                     continue\n#                 if key1 not in f_match:\n#                     f_match.create_group(key1)\n#                 if key2 not in f_match[key1]:\n#                     f_match[key1].create_dataset(key2, data=mkpts)\n#             except Exception as e:\n#                 if verbose:\n#                     print(f\"RoMa failed on {key1}-{key2}: {e}\")\n#                 continue\n\n#     del roma_model\n#     gc.collect()\n#     torch.cuda.empty_cache()","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:44.208124Z","iopub.status.busy":"2026-05-01T20:38:44.207917Z","iopub.status.idle":"2026-05-01T20:38:44.211335Z","shell.execute_reply":"2026-05-01T20:38:44.210611Z"},"papermill":{"duration":0.016735,"end_time":"2026-05-01T20:38:44.212472","exception":false,"start_time":"2026-05-01T20:38:44.195737","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"42744ab7","cell_type":"markdown","source":"# Pipeline: Image matching","metadata":{"papermill":{"duration":0.011231,"end_time":"2026-05-01T20:38:44.237155","exception":false,"start_time":"2026-05-01T20:38:44.225924","status":"completed"},"tags":[]}},{"id":"5113ca4e","cell_type":"markdown","source":"\n## New notebook flow\n\nFor each dataset:\n1. build DINOv2 retrieval embeddings\n2. create early scene clusters from the retrieval graph\n3. verify pairs cheaply with ALIKED\n4. run one main ALIKED + LightGlue pass\n5. reconstruct per cluster\n6. run a small recovery pass only for images that remain unregistered\n","metadata":{"papermill":{"duration":0.011462,"end_time":"2026-05-01T20:38:44.260377","exception":false,"start_time":"2026-05-01T20:38:44.248915","status":"completed"},"tags":[]}},{"id":"32829abf","cell_type":"code","source":"\ndef _merge_match_files(index_pairs, images, feature_dir, files_keypoints):\n    keypoints_merger(\n        images,\n        index_pairs,\n        files_keypoints,\n        feature_dir = feature_dir,\n        filter_FundamentalMatrix = False,\n    )\n    return f'{feature_dir}/matches.h5'\n\n\ndef exec_feature_extraction(params):\n    try:\n        images = params.images\n        feature_dir = params.feature_dir\n        device = params.device\n\n        t = time()\n        shortlist_info = get_image_pairs_shortlist(images, device=device)\n        params.laptime_image_pairs = time() - t\n\n        params.image_pairs = shortlist_info[\"verified_pairs\"]\n        params.scene_clusters = shortlist_info[\"scene_clusters\"]\n        params.weak_images = shortlist_info[\"weak_images\"]\n        params.topk_neighbors = shortlist_info[\"topk_neighbors\"]\n\n        print(f'Shortlisting + early clustering produced {len(params.image_pairs)} final pairs in {params.laptime_image_pairs:.4f} sec')\n        print(f'Number of scene clusters: {len(params.scene_clusters)}')\n\n        if len(params.image_pairs) == 0:\n            print(\"No verified pairs found. Falling back to exhaustive pairing.\")\n            params.image_pairs = get_img_pairs_exhaustive(images)\n            params.scene_clusters = [sorted(images)]\n\n        gc.collect()\n\n        # ----------------------------\n        # Main sparse matching pass\n        # ----------------------------\n        t = time()\n        files_keypoints = []\n\n        main_cfg = CONFIG.main_match\n        main_file = f'{feature_dir}/aliked_lightglue_main_mkpts.h5'\n        matching_aliked_lightglue_rot(\n            images,\n            params.image_pairs,\n            rot = 0,\n            saved_file = main_file,\n            feature_dir = feature_dir,\n            num_features = main_cfg.max_num_keypoints,\n            resize_to = main_cfg.image_size,\n            device = device,\n            min_matches = main_cfg.min_matches,\n            verbose = True,\n        )\n        files_keypoints.append(main_file)\n\n        params.laptime_lightglue_4rots = time() - t\n        print(f'Main sparse matching completed in {params.laptime_lightglue_4rots:.4f} sec')\n\n        params.matches_file = _merge_match_files(params.image_pairs, images, feature_dir, files_keypoints)\n        gc.collect()\n        return params\n    except Exception as e:\n        print(f'Feature extraction failed for dataset \"{params.dataset}\": {e}')\n        traceback.print_exc()\n        gc.collect()\n        return None\n\n\ndef build_recovery_pairs(params):\n    if not CONFIG.enable_recovery:\n        return []\n\n    unregistered = [p.filename for p in params.predictions if p.rotation is None]\n    if len(unregistered) == 0:\n        return []\n\n    print(f\"Recovery stage: {len(unregistered)} unregistered images\")\n\n    image_to_idx = {img: i for i, img in enumerate(params.images)}\n    fname_lookup = {os.path.basename(img): img for img in params.images}\n    recovery_pairs = set()\n\n    if params.topk_neighbors is None:\n        return []\n\n    for filename in unregistered:\n        full_path = fname_lookup.get(filename)\n        if full_path is None:\n            continue\n        i = image_to_idx[full_path]\n        for j in params.topk_neighbors[i][:CONFIG.recovery_expand_topk]:\n            recovery_pairs.add((min(i, j), max(i, j)))\n\n    recovery_pairs = sorted(list(recovery_pairs))\n    print(f\"Recovery stage proposed {len(recovery_pairs)} extra pairs\")\n    return recovery_pairs\n\n\ndef exec_recovery_matching(params):\n    recovery_pairs = build_recovery_pairs(params)\n    if len(recovery_pairs) == 0:\n        return params\n\n    feature_dir = params.feature_dir\n    images = params.images\n    device = params.device\n    recovery_cfg = CONFIG.recovery_match\n\n    files_keypoints = []\n    for rot in ([0, 1] if recovery_cfg.use_rotation else [0]):\n        file_keypoints = f'{feature_dir}/aliked_lightglue_recovery_rot{rot}_mkpts.h5'\n        matching_aliked_lightglue_rot(\n            images,\n            recovery_pairs,\n            rot = rot,\n            saved_file = file_keypoints,\n            feature_dir = feature_dir,\n            num_features = recovery_cfg.max_num_keypoints,\n            resize_to = recovery_cfg.image_size,\n            device = device,\n            min_matches = recovery_cfg.min_matches,\n            verbose = True,\n        )\n        files_keypoints.append(file_keypoints)\n\n    params.recovery_pairs = recovery_pairs\n    # Merge recovery matches WITH the original main matches so COLMAP sees everything\n    all_h5 = [f'{feature_dir}/aliked_lightglue_main_mkpts.h5'] + files_keypoints\n    all_pairs = sorted(list(set(params.image_pairs + recovery_pairs)))\n    params.matches_file = _merge_match_files(all_pairs, images, feature_dir, all_h5)\n    gc.collect()\n    return params\n","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:44.284246Z","iopub.status.busy":"2026-05-01T20:38:44.284039Z","iopub.status.idle":"2026-05-01T20:38:44.295384Z","shell.execute_reply":"2026-05-01T20:38:44.294726Z"},"papermill":{"duration":0.024544,"end_time":"2026-05-01T20:38:44.296465","exception":false,"start_time":"2026-05-01T20:38:44.271921","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"5c5350f7","cell_type":"markdown","source":"# SfM Maps Merger","metadata":{"papermill":{"duration":0.011747,"end_time":"2026-05-01T20:38:44.320323","exception":false,"start_time":"2026-05-01T20:38:44.308576","status":"completed"},"tags":[]}},{"id":"2d88a5de","cell_type":"code","source":"# Merge maps of colmap\nimport argparse, shutil, subprocess, tempfile, sys\nfrom pathlib import Path\n\nimport networkx as nx\nimport pycolmap  # pip install pycolmap\n\n# ---------- util ----------------------------------------------------------------\ndef read_image_names(model_dir: Path):\n    \"\"\"\n    pycolmap 0.6.x では Reconstruction() で読み込む。\n    返り値: 画像ファイル名(set[str])\n    \"\"\"\n    rec = pycolmap.Reconstruction(str(model_dir))   # ← これで images.bin をパース\n    return {img.name for img in rec.images.values()}\n\n#def read_image_names(model_dir: Path):\n#    images = pycolmap.read_images_binary(model_dir / \"images.bin\")\n#    return {img.name for img in images.values()}\n\ndef build_overlap_graph(image_sets, min_common):\n    G = nx.Graph()\n    G.add_nodes_from(range(len(image_sets)))\n    for i in range(len(image_sets)):\n        for j in range(i + 1, len(image_sets)):\n            if len(image_sets[i] & image_sets[j]) >= min_common:\n                G.add_edge(i, j)\n    return G\n\ndef run_cmd(cmd):\n    \"\"\"stdout+stderr をストリーム出力しつつ実行\"\"\"\n    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n    for line in proc.stdout:\n        print(line, end=\"\")\n    proc.wait()\n    if proc.returncode != 0:\n        raise subprocess.CalledProcessError(proc.returncode, cmd)\n\n# ---------- COLMAP wrappers ------------------------------------------------------\ndef merge_two_models(colmap_bin, ref_model, src_model, out_dir):\n    cmd = [\n        colmap_bin, \"model_merger\",\n        \"--input_path1\", str(ref_model),\n        \"--input_path2\", str(src_model),\n        \"--output_path\", str(out_dir),\n    ]\n    try:\n        run_cmd(cmd)\n        return True\n    except subprocess.CalledProcessError:\n        return False\n\ndef run_bundle_adjuster(colmap_bin, model_dir, num_threads=-1):\n    \"\"\"\n    実行例:\n        colmap bundle_adjuster --input_path scene_000 --output_path scene_000 \\\n            --BundleAdjustment.refine_principal_point 1\n    \"\"\"\n    cmd = [\n        colmap_bin, \"bundle_adjuster\",\n        \"--input_path\", str(model_dir),\n        \"--output_path\", str(model_dir),\n        \"--BundleAdjustment.refine_focal_length\", \"1\",\n        \"--BundleAdjustment.refine_principal_point\", \"1\",\n        \"--BundleAdjustment.refine_extra_params\", \"1\",\n    ]\n    if num_threads > 0:\n        cmd += [\"--Mapper.num_threads\", str(num_threads)]\n    run_cmd(cmd)     # raise on error\n\n# ---------- main logic -----------------------------------------------------------\ndef cluster_and_merge(\n    model_dirs,\n    out_root,\n    colmap_bin=\"colmap\",\n    min_common=10,\n    run_bundle_adjustment=True,\n    keep_tmp=False,\n):\n    out_root = Path(out_root)\n    out_root.mkdir(parents=True, exist_ok=True)\n\n    # 1) 画像集合\n    image_sets = [read_image_names(Path(m)) for m in model_dirs]\n\n    # 2) 重複グラフ → 連結成分\n    clusters = list(nx.connected_components(build_overlap_graph(image_sets, min_common)))\n    print(f\"num of clusters = {len(clusters)}\")\n    \n    merged_paths = []\n    for cidx, comp in enumerate(clusters):\n        comp = list(comp)\n        if len(comp) == 1:\n            src = Path(model_dirs[comp[0]])\n            dst = out_root / f\"scene_{cidx:03d}\"\n            shutil.copytree(src, dst, dirs_exist_ok=True)\n            merged_paths.append(dst)\n            if run_bundle_adjustment:\n                run_bundle_adjuster(colmap_bin, dst)\n            continue\n\n        # 3) 画像数の多い順に\n        comp_sorted = sorted(comp, key=lambda i: len(image_sets[i]), reverse=True)\n        work_dir = Path(model_dirs[comp_sorted[0]])\n\n        # 4) 順次マージ\n        failed_sources = []\n        for midx in comp_sorted[1:]:\n            tmp_dir = Path(tempfile.mkdtemp(prefix=f\"merge_s{cidx}_\"))\n            ok = merge_two_models(colmap_bin, work_dir, Path(model_dirs[midx]), tmp_dir)\n            if ok:\n                work_dir = tmp_dir\n            else:\n                failed_sources.append(midx)\n\n        # 5) 出力 & BA\n        final_dir = out_root / f\"scene_{cidx:03d}\"\n                # CHANGE LINE 118:\n        if os.path.exists(final_dir):\n            shutil.rmtree(final_dir)\n        shutil.move(str(work_dir), final_dir)\n        merged_paths.append(final_dir)\n\n        if run_bundle_adjustment:\n            run_bundle_adjuster(colmap_bin, final_dir)\n\n        # 6) マージ失敗分を別シーンとしてコピー\n        for fidx in failed_sources:\n            dst = out_root / f\"scene_{cidx}_{fidx}\"\n            shutil.copytree(model_dirs[fidx], dst, dirs_exist_ok=True)\n            merged_paths.append(dst)\n            if run_bundle_adjustment:\n                run_bundle_adjuster(colmap_bin, dst)\n\n    return merged_paths","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:44.346104Z","iopub.status.busy":"2026-05-01T20:38:44.345851Z","iopub.status.idle":"2026-05-01T20:38:44.357766Z","shell.execute_reply":"2026-05-01T20:38:44.357153Z"},"papermill":{"duration":0.026366,"end_time":"2026-05-01T20:38:44.359104","exception":false,"start_time":"2026-05-01T20:38:44.332738","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"35023da1","cell_type":"markdown","source":"# Update submission values","metadata":{"papermill":{"duration":0.012221,"end_time":"2026-05-01T20:38:44.383419","exception":false,"start_time":"2026-05-01T20:38:44.371198","status":"completed"},"tags":[]}},{"id":"c095c247","cell_type":"code","source":"def update_prediction(params, result_map_dirs, retry):\n    # Parse\n    feature_dir = params.feature_dir\n    images_dir = params.images_dir\n    filename_to_index = params.filename_to_index\n    predictions = params.predictions\n    dataset = params.dataset\n\n    maps = {}\n    for idx, result_map_dir in enumerate(result_map_dirs):\n        maps[idx] = pycolmap.Reconstruction(result_map_dir)\n    \n    print (\"Counting map size...\")\n    list_num_images = []\n    if isinstance(maps, dict):\n        for idx1, rec in maps.items():\n            list_num_images.append( len(rec.images) )\n    list_num_images = np.array(list_num_images)\n    print(f\"list_num_images = {list_num_images}\")\n    if params.list_model_size is None:\n        params.list_model_size = [list_num_images]\n    else:\n        params.list_model_size.append( list_num_images )\n    sort_idx = np.argsort( np.array(list_num_images) )\n    \n    registered = 0\n    for map_index, cur_idx in enumerate(sort_idx):\n        cur_map = maps[cur_idx]\n        cur_map_size = list_num_images[cur_idx]\n        if cur_map_size < 4:\n            continue\n        for index, image in cur_map.images.items():\n            prediction_index = filename_to_index[image.name]\n            if (cur_map_size > predictions[prediction_index].map_size) and (cur_map_size > 5):\n                predictions[prediction_index].map_size = cur_map_size\n                predictions[prediction_index].cluster_index = map_index\n                predictions[prediction_index].retry_index = retry\n                predictions[prediction_index].rotation = deepcopy(image.cam_from_world.rotation.matrix())\n                predictions[prediction_index].translation = deepcopy(image.cam_from_world.translation)\n                registered += 1\n    del maps\n    gc.collect()\n    return params","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:44.409193Z","iopub.status.busy":"2026-05-01T20:38:44.408936Z","iopub.status.idle":"2026-05-01T20:38:44.415765Z","shell.execute_reply":"2026-05-01T20:38:44.414748Z"},"papermill":{"duration":0.021262,"end_time":"2026-05-01T20:38:44.417322","exception":false,"start_time":"2026-05-01T20:38:44.396060","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"70cd6487","cell_type":"markdown","source":"# Clustering","metadata":{"papermill":{"duration":0.011671,"end_time":"2026-05-01T20:38:44.441228","exception":false,"start_time":"2026-05-01T20:38:44.429557","status":"completed"},"tags":[]}},{"id":"9973fe94","cell_type":"markdown","source":"# Pipeline: Cluster-Aware Multi-Scene SfM\n\nReconstruction now uses the **early retrieval clusters first**, then optionally runs a **recovery matching stage** for unregistered images.\n","metadata":{"papermill":{"duration":0.012104,"end_time":"2026-05-01T20:38:44.465998","exception":false,"start_time":"2026-05-01T20:38:44.453894","status":"completed"},"tags":[]}},{"id":"eee11c6d","cell_type":"code","source":"import networkx as nx\nfrom networkx.algorithms.community import louvain_communities\nfrom sklearn.cluster import SpectralClustering\nfrom sklearn.cluster import AgglomerativeClustering\nfrom typing import List, Tuple, Iterable\n\ndef get_network_from_matches_h5( file, images, num_max_keypoints = 8192, th_matches=150):\n    image_to_index = {file:i for i,file in enumerate(images)}\n    index_to_image = {i:file for i,file in enumerate(images)}\n\n    edges = []\n    with h5py.File(file, \"r\") as f_mat:\n        for key1 in f_mat.keys():\n            for key2 in f_mat[key1].keys():\n                if f_mat[ key1 ][ key2 ].shape[0] >=th_matches:\n                    edges.append( (key1, key2, f_mat[key1][key2].shape[0] / num_max_keypoints) )\n    G = nx.Graph()\n    G.add_weighted_edges_from(edges, weight=\"weight\")\n    return G, image_to_index, index_to_image\n\ndef get_components(\n    G: nx.Graph | nx.DiGraph,\n    *,\n    directed_mode: str = \"auto\"  # \"auto\" | \"weak\" | \"strong\"\n) -> Tuple[int, List[List]]:\n    # --- 無向グラフ ----------------------------------------------------\n    if not G.is_directed():\n        comp_iter: Iterable[set] = nx.connected_components(G)\n\n    # --- 有向グラフ ----------------------------------------------------\n    else:\n        if directed_mode == \"strong\":\n            comp_iter = nx.strongly_connected_components(G)\n        elif directed_mode in {\"weak\", \"auto\"}:\n            comp_iter = nx.weakly_connected_components(G)\n        else:\n            raise ValueError(\"directed_mode must be 'auto', 'weak', or 'strong'\")\n\n    components = [sorted(list(c)) for c in comp_iter]\n    n_components = len(components)\n    return n_components, components\n\ndef get_scenes_from_graph(G, random_state=42, th=0.2):\n    communities = louvain_communities(\n        G,\n        weight=\"weight\",\n        resolution=th,\n        seed=random_state\n    )\n    \n    scenes = []\n    print(f\"#clusters = {len(communities)}\")\n    for cid, members in enumerate(communities):\n        scene = set(members)\n        print(f\"Cluster {cid}: {len(members)} | {sorted(members)}\")\n        if len(scene) >= 5:\n            scenes.append( scene )\n        print(\"=\"*50)\n    return scenes","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:44.490609Z","iopub.status.busy":"2026-05-01T20:38:44.490322Z","iopub.status.idle":"2026-05-01T20:38:44.782256Z","shell.execute_reply":"2026-05-01T20:38:44.781239Z"},"papermill":{"duration":0.30561,"end_time":"2026-05-01T20:38:44.783742","exception":false,"start_time":"2026-05-01T20:38:44.478132","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"831f9895","cell_type":"code","source":"\ndef reconstruction_single(params, scene, retry, min_model_size):\n    feature_dir = params.feature_dir\n    images_dir = params.images_dir\n    filename_to_index = params.filename_to_index\n    predictions = params.predictions\n\n    database_path = os.path.join(feature_dir, f'colmap_{retry}.db')\n    if os.path.isfile(database_path):\n        os.remove(database_path)\n    gc.collect()\n    sleep(1)\n\n    import_into_colmap_with_scene(images_dir, scene, feature_dir=feature_dir, database_path=database_path)\n    output_path = f'{feature_dir}/colmap_rec_{retry}'\n\n    t = time()\n    pycolmap.match_exhaustive(database_path)\n    calc_fmat_time = time() - t\n    params.laptime_calc_fmat += calc_fmat_time\n    print(f'Ran RANSAC in {calc_fmat_time:.4f} sec')\n\n    mapper_options = pycolmap.IncrementalPipelineOptions()\n    mapper_options.min_model_size = min_model_size\n    mapper_options.max_num_models = 15\n    mapper_options.init_num_trials = 1000\n\n    os.makedirs(output_path, exist_ok=True)\n    t = time()\n    maps = pycolmap.incremental_mapping(\n        database_path=database_path,\n        image_path=images_dir,\n        output_path=output_path,\n        options=mapper_options)\n    sleep(1)\n    sfm_time = time() - t\n    params.laptime_sfm += sfm_time\n    print(f'Reconstruction done in  {sfm_time:.4f} sec')\n    print(maps)\n\n    clear_output(wait=False)\n\n    print(\"Counting map size...\")\n    list_num_images = []\n    if isinstance(maps, dict):\n        for idx1, rec in maps.items():\n            list_num_images.append(len(rec.images))\n    list_num_images = np.array(list_num_images)\n    print(f\"list_num_images = {list_num_images}\")\n    if params.list_model_size is None:\n        params.list_model_size = [list_num_images]\n    else:\n        params.list_model_size.append(list_num_images)\n    sort_idx = np.argsort(np.array(list_num_images))\n\n    registered = 0\n    for map_index, cur_idx in enumerate(sort_idx):\n        cur_map = maps[cur_idx]\n        cur_map_size = list_num_images[cur_idx]\n        for index, image in cur_map.images.items():\n            prediction_index = filename_to_index[image.name]\n            if (cur_map_size > predictions[prediction_index].map_size) and (cur_map_size > 5):\n                predictions[prediction_index].map_size = cur_map_size\n                predictions[prediction_index].cluster_index = map_index\n                predictions[prediction_index].retry_index = retry\n                predictions[prediction_index].rotation = deepcopy(image.cam_from_world.rotation.matrix())\n                predictions[prediction_index].translation = deepcopy(image.cam_from_world.translation)\n                registered += 1\n    gc.collect()\n\n    return params\n\n\ndef exec_reconstruction(params):\n    feature_dir = params.feature_dir\n    params.laptime_calc_fmat = 0.0\n    params.laptime_sfm = 0.0\n\n    # Use retrieval clusters first. Fallback to match graph components if needed.\n    if params.scene_clusters is not None and len(params.scene_clusters) > 0:\n        scenes = [set([os.path.basename(x) for x in cluster]) for cluster in params.scene_clusters]\n    else:\n        fnames = [p.filename for p in params.predictions]\n        matches_file = params.matches_file if params.matches_file is not None else f'{feature_dir}/matches.h5'\n        G, images_to_index, index_to_image = get_network_from_matches_h5(\n            matches_file, fnames, num_max_keypoints=CONFIG.main_match.max_num_keypoints, th_matches=100,\n        )\n        n_components, components = get_components(G)\n        scenes = [set(component) for component in components if len(component) >= CONFIG.min_cluster_size]\n\n    print(f\"Reconstructing {len(scenes)} scene clusters\")\n    for retry, scene in enumerate(scenes):\n        params = reconstruction_single(params, scene, retry, 2)\n\n    # Recovery stage only after first reconstruction pass\n    if CONFIG.enable_recovery:\n        before_registered = sum([1 for p in params.predictions if p.rotation is not None])\n        params = exec_recovery_matching(params)\n        after_match_recovery = params.matches_file is not None\n\n        if after_match_recovery:\n            recovery_scene = set([p.filename for p in params.predictions])\n            params = reconstruction_single(params, recovery_scene, len(scenes), 2)\n            after_registered = sum([1 for p in params.predictions if p.rotation is not None])\n            print(f\"Recovery stage registered {after_registered - before_registered} additional images\")\n\n    # merge models\n    output_dir = f\"{feature_dir}/colmap_rec_merged/\"\n    \n    if os.path.isdir(output_dir):\n        shutil.rmtree(output_dir)\n    \n    input_map_dirs = sorted(glob.glob(f\"{feature_dir}/colmap_rec_[0-9]*/*/cameras.bin\"))\n    input_map_dirs = [\n        os.path.dirname(x)\n        for x in input_map_dirs\n        if len(pycolmap.Reconstruction(os.path.dirname(x)).images) > 8\n    ]\n    input_map_dirs = [\n        x\n        for x in input_map_dirs\n        if pycolmap.Reconstruction(x).compute_mean_reprojection_error() < 2.0\n    ]\n\n    if len(input_map_dirs) > 1:\n        output_dir = f\"{feature_dir}/colmap_rec_merged/\"\n        COLMAP_BIN = \"/usr/bin/colmap\"\n        result_map_dirs = cluster_and_merge(\n            input_map_dirs,\n            output_dir,\n            colmap_bin=COLMAP_BIN,\n            min_common=6,\n            run_bundle_adjustment=True,\n            keep_tmp=False,\n        )\n        update_prediction(params, result_map_dirs, len(scenes))\n\n    clear_output(wait=False)\n    return params\n","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:44.809638Z","iopub.status.busy":"2026-05-01T20:38:44.809344Z","iopub.status.idle":"2026-05-01T20:38:44.823477Z","shell.execute_reply":"2026-05-01T20:38:44.822769Z"},"papermill":{"duration":0.028291,"end_time":"2026-05-01T20:38:44.824539","exception":false,"start_time":"2026-05-01T20:38:44.796248","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"e38d5d44","cell_type":"markdown","source":"# Main","metadata":{"papermill":{"duration":0.011414,"end_time":"2026-05-01T20:38:44.847547","exception":false,"start_time":"2026-05-01T20:38:44.836133","status":"completed"},"tags":[]}},{"id":"68fa8826","cell_type":"code","source":"\n# Collect vital info from the dataset\n\n\n@dataclasses.dataclass\nclass Prediction:\n    image_id: str | None\n    dataset: str\n    filename: str\n    map_size: int = -1\n    cluster_index: int | None = None\n    retry_index: int = -1\n    rotation: np.ndarray | None = None\n    translation: np.ndarray | None = None\n\n\n@dataclasses.dataclass\nclass DatasetParams:\n    dataset: str\n    feature_dir: str | None = None\n    images_dir: str | None = None\n    filename_to_index: dict | None = None\n    predictions: list | None = None\n    images: list | None = None\n    device: str | None = None\n    laptime_image_pairs: float = -1.0\n    laptime_lightglue_4rots: float = -1.0\n    laptime_roma: float = -1.0\n    laptime_calc_fmat: float = -1.0\n    laptime_sfm: float = -1.0\n    image_pairs: list | None = None\n    list_model_size: list | None = None\n    scene_clusters: list | None = None\n    weak_images: list | None = None\n    topk_neighbors: list | None = None\n    matches_file: str | None = None\n    recovery_pairs: list | None = None\n\n    def summary(self):\n        print(\"[Summary]\")\n        print(f\"- Dataset                   : {self.dataset}\")\n        if self.images is not None:\n            print(f\"- images                    : {len(self.images)}\")\n        if self.image_pairs is not None:\n            print(f\"- verified pairs            : {len(self.image_pairs)}\")\n        if self.scene_clusters is not None:\n            print(f\"- scene clusters            : {len(self.scene_clusters)}\")\n        if self.weak_images is not None:\n            print(f\"- weak images               : {len(self.weak_images)}\")\n        if self.recovery_pairs is not None:\n            print(f\"- recovery pairs            : {len(self.recovery_pairs)}\")\n        print(f\"- laptime_image_pairs       : {float(self.laptime_image_pairs):.2f}\")\n        print(f\"- laptime_lightglue_main    : {float(self.laptime_lightglue_4rots):.2f}\")\n        print(f\"- laptime_roma              : {float(self.laptime_roma):.2f}\")\n        print(f\"- laptime_calc_fmat         : {float(self.laptime_calc_fmat):.2f}\")\n        print(f\"- laptime_sfm               : {float(self.laptime_sfm):.2f}\")\n        print(f\"- list_model_size           : {self.list_model_size}\")\n\n# Set is_train=True to run the notebook on the training data.\nis_train = True\ndata_dir = '/kaggle/input/competitions/image-matching-challenge-2025-ongoing'\nworkdir = '/kaggle/working/'\n\nsample_submission_csv = os.path.join(data_dir, 'train_labels.csv') if is_train else os.path.join(data_dir, 'sample_submission.csv')\n\nsamples = {}\ncompetition_data = pd.read_csv(sample_submission_csv)\nfor _, row in competition_data.iterrows():\n    if row.dataset not in samples:\n        samples[row.dataset] = []\n    samples[row.dataset].append(\n        Prediction(\n            image_id=None if is_train else row.image_id,\n            dataset=row.dataset,\n            filename=row.image\n        )\n    )\n\ndataset_logs = []\n","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:44.871202Z","iopub.status.busy":"2026-05-01T20:38:44.870939Z","iopub.status.idle":"2026-05-01T20:38:45.023600Z","shell.execute_reply":"2026-05-01T20:38:45.022600Z"},"papermill":{"duration":0.166116,"end_time":"2026-05-01T20:38:45.025045","exception":false,"start_time":"2026-05-01T20:38:44.858929","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"cab05a9e","cell_type":"code","source":"import shutil\nworkdir = '/kaggle/working/'\nshutil.rmtree(os.path.join(workdir, 'featureout'), ignore_errors=True)\nos.makedirs(os.path.join(workdir, 'featureout'), exist_ok=True)\n\ngc.collect()\n\n# REPORT VISUALIZATION RUN SETTINGS\n# Keep all ETs images. Do not crop the dataset to 12/20/etc.\nmax_images = None\n\n# Run only ETs because we only need report images, not a full CV run over every dataset.\ndatasets_to_process = ['ETs']\n\nmapping_result_strs = []\n\n\n# Enqeue feature extraction processing\nfutures_gpu0 = {}\nfutures_gpu1 = {}\ndevice = None\ndevice_index = None\nwith concurrent.futures.ThreadPoolExecutor(max_workers=1) as executors_gpu0, \\\n     concurrent.futures.ThreadPoolExecutor(max_workers=1) as executors_gpu1:\n\n    executors_gpus = [executors_gpu0, executors_gpu1]\n    futures_gpus = [futures_gpu0, futures_gpu1]\n    for dataset, predictions in samples.items():\n        if datasets_to_process and dataset not in datasets_to_process:\n            print(f'Skipping \"{dataset}\"')\n            continue\n        \n        images_dir = os.path.join(data_dir, 'train' if is_train else 'test', dataset)\n        images = [os.path.join(images_dir, p.filename) for p in predictions]\n        if max_images is not None:\n            images = images[:max_images]\n    \n        print(f'\\nProcessing dataset \"{dataset}\": {len(images)} images')\n    \n        filename_to_index = {p.filename: idx for idx, p in enumerate(predictions)}\n    \n        feature_dir = os.path.join(workdir, 'featureout', dataset)\n        os.makedirs(feature_dir, exist_ok=True)\n\n        # Switch device\n        device, device_index = switch_gpu(device, device_index)\n        \n        # Dataset parameter\n        dataset_params = DatasetParams(\n            dataset = dataset,\n            feature_dir = feature_dir,\n            images_dir = images_dir,\n            predictions = predictions,\n            filename_to_index = filename_to_index,\n            images = images,  # image filepaths\n            device = device,\n        )\n\n        # Enqueue feature extraction\n        futures_gpus[device_index][dataset] = executors_gpus[device_index].submit(\n            exec_feature_extraction, dataset_params,\n        )\n        #dataset_params = exec_feature_extraction(dataset_params)\n\n    # Enqeue reconstruction processing\n    futures_cpu  = {}\n    with concurrent.futures.ThreadPoolExecutor(max_workers=CONFIG.num_pallalel_sfm) as executors:\n        for dataset, predictions in samples.items():\n            if datasets_to_process and (dataset not in datasets_to_process):\n                print(f'Skipping \"{dataset}\"')\n                continue\n                \n            if dataset in futures_gpu0.keys():\n                future = futures_gpu0[dataset]\n            else:\n                future = futures_gpu1[dataset]\n    \n            # Wait for feature extraction\n            print(f\"waiting feature extraction at dataset = {dataset}\")\n            dataset_params = future.result()\n    \n            # if feature extraction is failed:\n            if dataset_params is None:\n                continue\n                \n            # Enqueue reconstruction\n            gc.collect()\n            futures_cpu[dataset] = executors.submit(\n                exec_reconstruction, dataset_params,\n            )\n\n        # Wait to reconstruction\n        for dataset, predictions in samples.items():\n            if datasets_to_process and (dataset not in datasets_to_process):\n                print(f'Skipping \"{dataset}\"')\n                continue\n            gc.collect()\n            if dataset not in futures_cpu:\n                print(f'Skipping \"{dataset}\" (feature extraction failed)')\n                continue\n            dataset_params = futures_cpu[dataset].result()\n            if dataset_params is None:\n                print(f'Skipping \"{dataset}\" (reconstruction returned None)')\n                continue\n            samples[dataset] = dataset_params.predictions\n            dataset_logs.append( dataset_params )\n            gc.collect()","metadata":{"execution":{"iopub.execute_input":"2026-05-01T20:38:45.049637Z","iopub.status.busy":"2026-05-01T20:38:45.049373Z","iopub.status.idle":"2026-05-02T00:32:42.402372Z","shell.execute_reply":"2026-05-02T00:32:42.401167Z"},"papermill":{"duration":14037.37162,"end_time":"2026-05-02T00:32:42.408492","exception":false,"start_time":"2026-05-01T20:38:45.036872","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"be7d308d-0cf7-4751-a652-abf5809143cc","cell_type":"markdown","source":"# Report visualizations: feature matches\n\nThis section saves side-by-side feature match images for the ETs scene. It reads the raw ALIKED + LightGlue match coordinates from `aliked_lightglue_main_mkpts.h5` first, because the merged `matches.h5` stores COLMAP match indices rather than drawable x/y coordinates.\n","metadata":{}},{"id":"314684d3-3fee-4b29-9ab5-c051061be074","cell_type":"code","source":"\n# ============================================================\n# REPORT VISUALIZATION: draw feature matches for ETs\n# ============================================================\n# Output folder:\n# /kaggle/working/featureout/ETs/report_match_visuals/\n# Zip file:\n# /kaggle/working/report_match_visuals_ETs.zip\n\nimport os, glob, math, shutil\nimport numpy as np\nimport h5py\nfrom PIL import Image as PILImage, ImageDraw\nfrom IPython.display import display, Image as IPyImage\n\nREPORT_VIS_DATASET = 'ETs'\nREPORT_VIS_MAX_PAIRS = 12          # number of pair-visualization PNGs to save; set None for all matched pairs\nREPORT_VIS_MATCHES_PER_PAIR = 80   # lines/dots per image pair; set higher if you want denser figures\nREPORT_VIS_MIN_MATCHES = 15\nREPORT_VIS_SEED = 7\n\n# Important: this keeps the original full image sizes. We do not limit ETs images.\nREPORT_VIS_RESIZE_MAX_SIDE = None\n\n\ndef _resize_if_needed(img, max_side=None):\n    \"\"\"Resize only if max_side is given. Returns image and (scale_x, scale_y).\"\"\"\n    if max_side is None:\n        return img, 1.0, 1.0\n    w, h = img.size\n    side = max(w, h)\n    if side <= max_side:\n        return img, 1.0, 1.0\n    scale = max_side / float(side)\n    new_size = (max(1, int(round(w * scale))), max(1, int(round(h * scale))))\n    img = img.resize(new_size, PILImage.LANCZOS)\n    return img, scale, scale\n\n\ndef _safe_filename(name):\n    return ''.join(ch if ch.isalnum() or ch in ('-', '_', '.') else '_' for ch in name)\n\n\ndef _iter_raw_match_pairs(h5_path):\n    \"\"\"Yield (image1, image2, raw_matches Nx4/Nx5, source_path) from raw match-coordinate H5 files.\"\"\"\n    if not os.path.isfile(h5_path):\n        return\n    try:\n        with h5py.File(h5_path, 'r') as f:\n            for key1 in f.keys():\n                obj = f[key1]\n                if not isinstance(obj, h5py.Group):\n                    continue\n                for key2 in obj.keys():\n                    arr = np.asarray(obj[key2])\n                    if arr.ndim == 2 and arr.shape[1] >= 4 and arr.shape[0] >= REPORT_VIS_MIN_MATCHES:\n                        yield key1, key2, arr, h5_path\n    except Exception as e:\n        print(f'Could not read raw match file {h5_path}: {e}')\n\n\ndef _iter_index_match_pairs(feature_dir):\n    \"\"\"Fallback: read keypoints.h5 + matches.h5 where matches are COLMAP index pairs.\"\"\"\n    kp_path = os.path.join(feature_dir, 'keypoints.h5')\n    match_path = os.path.join(feature_dir, 'matches.h5')\n    if not (os.path.isfile(kp_path) and os.path.isfile(match_path)):\n        return\n\n    try:\n        with h5py.File(kp_path, 'r') as f_kp, h5py.File(match_path, 'r') as f_match:\n            for key1 in f_match.keys():\n                if key1 not in f_kp:\n                    continue\n                obj = f_match[key1]\n                if not isinstance(obj, h5py.Group):\n                    continue\n                kpts1 = np.asarray(f_kp[key1])\n                for key2 in obj.keys():\n                    if key2 not in f_kp:\n                        continue\n                    idx = np.asarray(obj[key2])\n                    if idx.ndim != 2 or idx.shape[1] < 2 or idx.shape[0] < REPORT_VIS_MIN_MATCHES:\n                        continue\n                    idx = idx[:, :2].astype(np.int64)\n                    kpts2 = np.asarray(f_kp[key2])\n                    valid = (\n                        (idx[:, 0] >= 0) & (idx[:, 0] < len(kpts1)) &\n                        (idx[:, 1] >= 0) & (idx[:, 1] < len(kpts2))\n                    )\n                    idx = idx[valid]\n                    if len(idx) < REPORT_VIS_MIN_MATCHES:\n                        continue\n                    mkpts = np.concatenate([kpts1[idx[:, 0]], kpts2[idx[:, 1]]], axis=1)\n                    yield key1, key2, mkpts, match_path\n    except Exception as e:\n        print(f'Could not read fallback COLMAP match files: {e}')\n\n\ndef _collect_visualization_pairs(feature_dir):\n    # Preferred files: raw coordinates from ALIKED + LightGlue.\n    raw_candidates = []\n    raw_candidates.append(os.path.join(feature_dir, 'aliked_lightglue_main_mkpts.h5'))\n    raw_candidates.extend(sorted(glob.glob(os.path.join(feature_dir, 'aliked_lightglue_recovery_rot*_mkpts.h5'))))\n    raw_candidates.append(os.path.join(feature_dir, 'merge_tmp0.h5'))\n    raw_candidates.append(os.path.join(feature_dir, 'merge_tmp.h5'))\n\n    pairs = []\n    seen = set()\n    for path in raw_candidates:\n        for key1, key2, mkpts, source_path in _iter_raw_match_pairs(path):\n            pair_key = tuple(sorted([key1, key2]))\n            if pair_key in seen:\n                continue\n            seen.add(pair_key)\n            pairs.append((key1, key2, mkpts, source_path))\n\n    if len(pairs) > 0:\n        print(f'Using raw coordinate matches from ALIKED/LightGlue files. Found {len(pairs)} drawable pairs.')\n        return pairs\n\n    # Fallback: matches.h5 contains indices, so convert indices back to x/y using keypoints.h5.\n    pairs = list(_iter_index_match_pairs(feature_dir))\n    if len(pairs) > 0:\n        print(f'Using fallback keypoints.h5 + matches.h5. Found {len(pairs)} drawable pairs.')\n    return pairs\n\n\ndef _choose_matches_to_draw(mkpts, max_matches=80, seed=7):\n    if mkpts.shape[0] <= max_matches:\n        return mkpts\n\n    # If confidence exists in column 4, use strongest matches. Otherwise sample evenly.\n    if mkpts.shape[1] >= 5:\n        order = np.argsort(-mkpts[:, 4])[:max_matches]\n        return mkpts[order]\n\n    rng = np.random.default_rng(seed)\n    idx = rng.choice(mkpts.shape[0], size=max_matches, replace=False)\n    return mkpts[idx]\n\n\ndef _draw_match_visual(image1_path, image2_path, mkpts, out_path, title=None):\n    img1 = PILImage.open(image1_path).convert('RGB')\n    img2 = PILImage.open(image2_path).convert('RGB')\n\n    img1, sx1, sy1 = _resize_if_needed(img1, REPORT_VIS_RESIZE_MAX_SIDE)\n    img2, sx2, sy2 = _resize_if_needed(img2, REPORT_VIS_RESIZE_MAX_SIDE)\n\n    gap = 30\n    w1, h1 = img1.size\n    w2, h2 = img2.size\n    header = 36 if title else 0\n    canvas = PILImage.new('RGB', (w1 + gap + w2, max(h1, h2) + header), (255, 255, 255))\n    canvas.paste(img1, (0, header))\n    canvas.paste(img2, (w1 + gap, header))\n\n    draw = ImageDraw.Draw(canvas)\n    if title:\n        draw.text((8, 8), title, fill=(0, 0, 0))\n\n    mkpts_draw = _choose_matches_to_draw(mkpts, REPORT_VIS_MATCHES_PER_PAIR, REPORT_VIS_SEED)\n\n    # Deterministic but varied colors for report readability.\n    rng = np.random.default_rng(REPORT_VIS_SEED)\n    colors = rng.integers(low=30, high=230, size=(len(mkpts_draw), 3))\n\n    for row, color in zip(mkpts_draw, colors):\n        x1, y1, x2, y2 = row[:4]\n        x1 = float(x1) * sx1\n        y1 = float(y1) * sy1 + header\n        x2 = float(x2) * sx2 + w1 + gap\n        y2 = float(y2) * sy2 + header\n        color = tuple(int(c) for c in color)\n        draw.line((x1, y1, x2, y2), fill=color, width=2)\n        r = 4\n        draw.ellipse((x1 - r, y1 - r, x1 + r, y1 + r), outline=color, width=2)\n        draw.ellipse((x2 - r, y2 - r, x2 + r, y2 + r), outline=color, width=2)\n\n    canvas.save(out_path, quality=95)\n    return out_path\n\n\ndef make_report_match_visualizations(dataset='ETs'):\n    feature_dir = os.path.join('/kaggle/working/featureout', dataset)\n    images_dir = os.path.join(data_dir, 'train' if is_train else 'test', dataset)\n    out_dir = os.path.join(feature_dir, 'report_match_visuals')\n    os.makedirs(out_dir, exist_ok=True)\n\n    print('Feature folder:', feature_dir)\n    print('Images folder :', images_dir)\n    print('Output folder :', out_dir)\n\n    pairs = _collect_visualization_pairs(feature_dir)\n    if len(pairs) == 0:\n        print('No drawable matched pairs found.')\n        print('Expected one of these files to contain drawable matches:')\n        print(' -', os.path.join(feature_dir, 'aliked_lightglue_main_mkpts.h5'))\n        print(' -', os.path.join(feature_dir, 'merge_tmp0.h5'))\n        print(' -', os.path.join(feature_dir, 'keypoints.h5 + matches.h5'))\n        return []\n\n    # Strongest pairs first.\n    pairs = sorted(pairs, key=lambda x: x[2].shape[0], reverse=True)\n    if REPORT_VIS_MAX_PAIRS is not None:\n        pairs = pairs[:REPORT_VIS_MAX_PAIRS]\n\n    saved = []\n    for pair_no, (key1, key2, mkpts, source_path) in enumerate(pairs, start=1):\n        image1_path = os.path.join(images_dir, key1)\n        image2_path = os.path.join(images_dir, key2)\n        if not os.path.isfile(image1_path) or not os.path.isfile(image2_path):\n            print(f'Skipping pair because image file is missing: {key1}, {key2}')\n            continue\n\n        out_name = f'{pair_no:02d}_{_safe_filename(key1)}__MATCHES__{_safe_filename(key2)}.png'\n        out_path = os.path.join(out_dir, out_name)\n        title = f'{key1}  <->  {key2}    matches: {mkpts.shape[0]}    source: {os.path.basename(source_path)}'\n        _draw_match_visual(image1_path, image2_path, mkpts, out_path, title=title)\n        saved.append(out_path)\n\n    if len(saved) > 0:\n        zip_base = f'/kaggle/working/report_match_visuals_{dataset}'\n        if os.path.exists(zip_base + '.zip'):\n            os.remove(zip_base + '.zip')\n        shutil.make_archive(zip_base, 'zip', out_dir)\n        print(f'Saved {len(saved)} visualization images.')\n        print('Folder:', out_dir)\n        print('Zip   :', zip_base + '.zip')\n        print('Preview:')\n        display(IPyImage(filename=saved[0]))\n    else:\n        print('No images were saved.')\n\n    return saved\n\nreport_visual_paths = make_report_match_visualizations(REPORT_VIS_DATASET)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"5dd14f21","cell_type":"code","source":"# RoMa debug file walker removed because this sparse pipeline does not require pkg-roma.","metadata":{"execution":{"iopub.execute_input":"2026-05-02T00:32:42.439106Z","iopub.status.busy":"2026-05-02T00:32:42.438848Z","iopub.status.idle":"2026-05-02T00:32:42.442520Z","shell.execute_reply":"2026-05-02T00:32:42.441548Z"},"papermill":{"duration":0.018475,"end_time":"2026-05-02T00:32:42.444049","exception":false,"start_time":"2026-05-02T00:32:42.425574","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"8585c42d","cell_type":"markdown","source":"## Create submission","metadata":{"papermill":{"duration":0.011524,"end_time":"2026-05-02T00:32:42.467727","exception":false,"start_time":"2026-05-02T00:32:42.456203","status":"completed"},"tags":[]}},{"id":"fdefe83e","cell_type":"code","source":"# Must Create a submission file.\n\narray_to_str = lambda array: ';'.join([f\"{x:.09f}\" for x in array])\nnone_to_str = lambda n: ';'.join(['nan'] * n)\n\nsubmission_file = '/kaggle/working/submission.csv'\nwith open(submission_file, 'w') as f:\n    if is_train:\n        f.write('dataset,scene,image,rotation_matrix,translation_vector\\n')\n        for dataset in samples:\n            for prediction in samples[dataset]:\n                cluster_name = 'outliers' if prediction.cluster_index is None else f'retry{prediction.retry_index}cluster{prediction.cluster_index}'\n                rotation = none_to_str(9) if prediction.rotation is None else array_to_str(prediction.rotation.flatten())\n                translation = none_to_str(3) if prediction.translation is None else array_to_str(prediction.translation)\n                f.write(f'{prediction.dataset},{cluster_name},{prediction.filename},{rotation},{translation}\\n')\n    else:\n        f.write('image_id,dataset,scene,image,rotation_matrix,translation_vector\\n')\n        for dataset in samples:\n            for prediction in samples[dataset]:\n                cluster_name = 'outliers' if prediction.cluster_index is None else f'retry{prediction.retry_index}cluster{prediction.cluster_index}'\n                rotation = none_to_str(9) if prediction.rotation is None else array_to_str(prediction.rotation.flatten())\n                translation = none_to_str(3) if prediction.translation is None else array_to_str(prediction.translation)\n                f.write(f'{prediction.image_id},{prediction.dataset},{cluster_name},{prediction.filename},{rotation},{translation}\\n')\n\n!head {submission_file}","metadata":{"execution":{"iopub.execute_input":"2026-05-02T00:32:42.492168Z","iopub.status.busy":"2026-05-02T00:32:42.491891Z","iopub.status.idle":"2026-05-02T00:32:42.743424Z","shell.execute_reply":"2026-05-02T00:32:42.742527Z"},"papermill":{"duration":0.265744,"end_time":"2026-05-02T00:32:42.745106","exception":false,"start_time":"2026-05-02T00:32:42.479362","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"e46de421","cell_type":"markdown","source":"# Running logs","metadata":{"papermill":{"duration":0.011826,"end_time":"2026-05-02T00:32:42.770088","exception":false,"start_time":"2026-05-02T00:32:42.758262","status":"completed"},"tags":[]}},{"id":"b0d6c954","cell_type":"code","source":"for dataset_params in dataset_logs:\n    print(\"=\" * 100)\n    dataset_params.summary()","metadata":{"execution":{"iopub.execute_input":"2026-05-02T00:32:42.794310Z","iopub.status.busy":"2026-05-02T00:32:42.794028Z","iopub.status.idle":"2026-05-02T00:32:42.834286Z","shell.execute_reply":"2026-05-02T00:32:42.832624Z"},"papermill":{"duration":0.05378,"end_time":"2026-05-02T00:32:42.835441","exception":false,"start_time":"2026-05-02T00:32:42.781661","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"3ae2e2ca","cell_type":"markdown","source":"# CV scoring","metadata":{"papermill":{"duration":0.012801,"end_time":"2026-05-02T00:32:42.861877","exception":false,"start_time":"2026-05-02T00:32:42.849076","status":"completed"},"tags":[]}},{"id":"91f731af","cell_type":"code","source":"\n# Optional CV scoring.\n# For this report-visualization notebook we run only ETs, so full CV scoring is disabled by default.\nRUN_CV_SCORE = False\n\nif is_train and RUN_CV_SCORE:\n    t = time()\n    final_score, dataset_scores = metric.score(\n        gt_csv=os.path.join(data_dir, 'train_labels.csv'),\n        user_csv=submission_file,\n        thresholds_csv=os.path.join(data_dir, 'train_thresholds.csv'),\n        mask_csv=None if is_train else os.path.join(data_dir, 'mask.csv'),\n        inl_cf=0,\n        strict_cf=-1,\n        verbose=True,\n    )\n    print(f'Computed metric in: {time() - t:.02f} sec.')\nelse:\n    print('CV scoring skipped. This notebook is configured for ETs report visualizations only.')\n","metadata":{"execution":{"iopub.execute_input":"2026-05-02T00:32:42.887766Z","iopub.status.busy":"2026-05-02T00:32:42.887541Z","iopub.status.idle":"2026-05-02T01:05:08.290678Z","shell.execute_reply":"2026-05-02T01:05:08.289583Z"},"papermill":{"duration":1945.417528,"end_time":"2026-05-02T01:05:08.292135","exception":false,"start_time":"2026-05-02T00:32:42.874607","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"17ad8f43","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.086941,"end_time":"2026-05-02T01:05:08.393432","exception":false,"start_time":"2026-05-02T01:05:08.306491","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}