{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":91498,"databundleVersionId":11655853,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":11822514,"sourceType":"datasetVersion","datasetId":7426359},{"sourceId":11822532,"sourceType":"datasetVersion","datasetId":7426375},{"sourceId":4534,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":3326,"modelId":986},{"sourceId":17191,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":14317,"modelId":21716},{"sourceId":17555,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":14611,"modelId":22086},{"sourceId":332398,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":278629,"modelId":299532}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Install dependencies and copy model weights to run the notebook without internet access when submitting to the competition.\n\n!pip install --no-index --no-deps /kaggle/input/imc2024-packages-pycolmap-lightglue-rerun-kornia/kornia-0.7.2-py2.py3-none-any.whl\n!pip install --no-index --no-deps /kaggle/input/imc2024-packages-pycolmap-lightglue-rerun-kornia/kornia_moons-0.2.9-py3-none-any.whl\n!pip install --no-index --no-deps /kaggle/input/imc2024-packages-pycolmap-lightglue-rerun-kornia/lightglue-0.0-py3-none-any.whl\n!pip install --no-index --no-deps /kaggle/input/imc2024-packages-pycolmap-lightglue-rerun-kornia/rerun_sdk-0.15.0a2-cp38-abi3-manylinux_2_31_x86_64.whl\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-15T11:53:14.612721Z","iopub.execute_input":"2025-05-15T11:53:14.613700Z","iopub.status.idle":"2025-05-15T11:53:22.313221Z","shell.execute_reply.started":"2025-05-15T11:53:14.613663Z","shell.execute_reply":"2025-05-15T11:53:22.311605Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!sudo apt-get update\n!sudo apt-get install -y libboost-all-dev libsuitesparse-dev libglew-dev cmake\n\n# Clone and install pycolmap\n!git clone https://github.com/mihaidusmanu/pycolmap.git\n%cd pycolmap\n!pip install .\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-15T11:50:21.998127Z","iopub.execute_input":"2025-05-15T11:50:21.998496Z","iopub.status.idle":"2025-05-15T11:52:24.059446Z","shell.execute_reply.started":"2025-05-15T11:50:21.998464Z","shell.execute_reply":"2025-05-15T11:52:24.057891Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nimport os\nfrom tqdm import tqdm\nfrom time import time, sleep\nimport gc\nimport numpy as np\nimport h5py\nimport dataclasses\nimport pandas as pd\nfrom IPython.display import clear_output\nfrom collections import defaultdict\nfrom copy import deepcopy\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim\nfrom torch import Tensor\nfrom torch.utils.data import DataLoader, Dataset,TensorDataset\nimport warnings\n#avoid some negligible errors\n#The filterwarnings () method is used to set warning filters, which can control the output method and level of warning information.\nwarnings.filterwarnings('ignore')\nimport random#provide some function to generate random_seed.\n#set random seed,to make sure model can be recurrented.\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\nimport pycolmap\nsys.path.append('/kaggle/input/imc25-utils')\nfrom database import *\nfrom h5_to_db import *\nimport metric","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-15T11:53:37.446886Z","iopub.execute_input":"2025-05-15T11:53:37.447358Z","iopub.status.idle":"2025-05-15T11:53:37.458465Z","shell.execute_reply.started":"2025-05-15T11:53:37.447319Z","shell.execute_reply":"2025-05-15T11:53:37.456978Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = K.utils.get_cuda_device_if_available(0)\nprint(f'{device=}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-15T11:37:03.225723Z","iopub.status.idle":"2025-05-15T11:37:03.225933Z","shell.execute_reply.started":"2025-05-15T11:37:03.225832Z","shell.execute_reply":"2025-05-15T11:37:03.225842Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_torch_image(fname, device):\n    img = K.io.load_image(fname, K.io.ImageLoadType.RGB32, device=device)[None, ...]\n    return img\n\ndef get_global_desc(fnames, device):\n    processor = AutoImageProcessor.from_pretrained('/kaggle/input/dinov2/pytorch/base/1')\n    model = AutoModel.from_pretrained('/kaggle/input/dinov2/pytorch/base/1').eval().to(device)\n    descs = []\n    for img_path in tqdm(fnames, desc='Computing global descriptors'):\n        img = load_torch_image(img_path, device)\n        with torch.no_grad():\n            inputs = processor(images=img, return_tensors=\"pt\", do_rescale=False).to(device)\n            outputs = model(**inputs)\n            desc = F.normalize(outputs.last_hidden_state[:,1:].max(dim=1)[0], dim=1)\n        descs.append(desc.detach().cpu())\n    return torch.cat(descs, dim=0)\ndef get_image_pairs(fnames, sim_th=0.5, min_pairs=20, exhaustive_if_less=20):\n    if len(fnames) <= exhaustive_if_less:\n        return [(i, j) for i in range(len(fnames)) for j in range(i+1, len(fnames))]\n    descs = get_global_desc(fnames, device)\n    dists = torch.cdist(descs, descs).cpu().numpy()\n    pairs = []\n    for i in range(len(fnames)):\n        matches = np.where(dists[i] < sim_th)[0]\n        if len(matches) < min_pairs:\n            matches = np.argsort(dists[i])[:min_pairs]\n        for j in matches:\n            if i < j:\n                pairs.append((i, j))\n    return sorted(list(set(pairs)))\ndef detect_disk(fnames, feature_dir, num_features=5000):\n    os.makedirs(feature_dir, exist_ok=True)\n    disk = KF.DISK(n=num_features, score_threshold=0.1).eval().to(device)\n    with h5py.File(f'{feature_dir}/keypoints.h5', 'w') as f_kp, \\\n         h5py.File(f'{feature_dir}/descriptors.h5', 'w') as f_desc:\n        for img_path in tqdm(fnames, desc='Detecting keypoints with DISK'):\n            img = load_torch_image(img_path, device)\n            with torch.no_grad():\n                features = disk(img)\n                kpts = features.keypoints.cpu().numpy()\n                descs = features.descriptors.cpu().numpy()\n            key = os.path.basename(img_path)\n            f_kp[key] = kpts\n            f_desc[key] = descs\n\n\n\ndef match_superglue(fnames, pairs, feature_dir):\n    config = {\n        'weights': 'outdoor',\n        'sinkhorn_iterations': 100,\n        'match_threshold': 0.2,\n    }\n    matcher = SuperGlue(config).eval().to(device)\n    with h5py.File(f'{feature_dir}/keypoints.h5', 'r') as f_kp, \\\n         h5py.File(f'{feature_dir}/descriptors.h5', 'r') as f_desc, \\\n         h5py.File(f'{feature_dir}/matches.h5', 'w') as f_match:\n        for i, j in tqdm(pairs, desc='Matching with SuperGlue'):\n            fname1, fname2 = fnames[i], fnames[j]\n            key1, key2 = os.path.basename(fname1), os.path.basename(fname2)\n            kp1 = torch.from_numpy(f_kp[key1][...]).to(device)\n            desc1 = torch.from_numpy(f_kp[key1][...]).to(device)\n            kp2 = torch.from_numpy(f_kp[key2][...]).to(device)\n            desc2 = torch.from_numpy(f_kp[key2][...]).to(device)\n            data = {\n                'keypoints0': kp1[None],\n                'descriptors0': desc1[None],\n                'keypoints1': kp2[None],\n                'descriptors1': desc2[None]\n            }\n            with torch.no_grad():\n                pred = matcher(data)\n                matches = pred['matches0'][0].cpu().numpy()\n                valid = matches > -1\n                matches = np.stack([np.where(valid)[0], matches[valid]], axis=1)\n            if len(matches) >= 15:\n                group = f_match.require_group(key1)\n                group.create_dataset(key2, data=matches)\n\ndef geometric_verification(fnames, pairs, feature_dir):\n    with h5py.File(f'{feature_dir}/keypoints.h5', 'r') as f_kp, \\\n         h5py.File(f'{feature_dir}/matches.h5', 'r+') as f_match:\n        for i, j in tqdm(pairs, desc='Geometric verification'):\n            fname1, fname2 = fnames[i], fnames[j]\n            key1, key2 = os.path.basename(fname1), os.path.basename(fname2)\n            if key1 in f_match and key2 in f_match[key1]:\n                kp1 = f_kp[key1][...]\n                kp2 = f_kp[key2][...]\n                matches = f_match[key1][key2][...]\n                if len(matches) >= 8:\n                    _, inliers = cv2.findFundamentalMat(\n                        kp1[matches[:, 0]],\n                        kp2[matches[:, 1]],\n                        cv2.FM_RANSAC,\n                        1.0,\n                        0.999\n                    )\n                    if inliers is not None:\n                        valid = inliers.ravel() > 0\n                        if np.sum(valid) >= 15:\n                            f_match[key1][key2][...] = matches[valid]\n                        else:\n                            del f_match[key1][key2]\n\ndef import_into_colmap(img_dir, feature_dir, database_path):\n    db = COLMAPDatabase.connect(database_path)\n    db.create_tables()\n    fname_to_id = add_keypoints(db, feature_dir, img_dir, '', 'simple-pinhole', False)\n    add_matches(db, feature_dir, fname_to_id)\n    db.commit()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-15T11:41:42.514999Z","iopub.execute_input":"2025-05-15T11:41:42.515720Z","iopub.status.idle":"2025-05-15T11:41:42.534735Z","shell.execute_reply.started":"2025-05-15T11:41:42.515690Z","shell.execute_reply":"2025-05-15T11:41:42.534114Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import dataclasses\n\n@dataclasses.dataclass\nclass Prediction:\n    image_id: str\n    dataset: str\n    filename: str\n    cluster_index: int = None\n    rotation: np.ndarray = None\n    translation: np.ndarray = None\n\nis_train = False\ndata_dir = '/kaggle/input/image-matching-challenge-2025'\nwork_dir = '/kaggle/working/result'\nos.makedirs(work_dir, exist_ok=True)\n\nsubmission_csv = os.path.join(data_dir, 'train_labels.csv' if is_train else 'sample_submission.csv')\nsamples = {}\nfor _, row in pd.read_csv(submission_csv).iterrows():\n    if row.dataset not in samples:\n        samples[row.dataset] = []\n    samples[row.dataset].append(Prediction(\n        image_id=row.image_id if not is_train else None,\n        dataset=row.dataset,\n        filename=row.image\n    ))\n\nprint('Datasets loaded:')\nfor dataset in samples:\n    print(f'  - {dataset}: {len(samples[dataset])} images')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-15T11:37:03.228620Z","iopub.status.idle":"2025-05-15T11:37:03.228838Z","shell.execute_reply.started":"2025-05-15T11:37:03.228733Z","shell.execute_reply":"2025-05-15T11:37:03.228742Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from time import time\n\ntimings = {\n    'shortlisting': [],\n    'feature_detection': [],\n    'feature_matching': [],\n    'geometric_verification': [],\n    'reconstruction': []\n}\nresults = []\nprint('Starting processing...')\nfor dataset, predictions in samples.items():\n    print(f'\\nProcessing dataset: {dataset}')\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    \n    feature_dir = os.path.join(work_dir, 'featureout', dataset)\n    database_path = os.path.join(feature_dir, 'colmap.db')\n    output_path = os.path.join(feature_dir, 'colmap_rec')\n    \n    try:\n        # Find similar image pairs\n        t = time()\n        pairs = get_image_pairs(images, sim_th=0.5, min_pairs=20)\n        timings['shortlisting'].append(time() - t)\n        print(f'  - Shortlisted {len(pairs)} pairs in {time()-t:.2f}s')\n        # Detect features\n        t = time()\n        detect_disk(images, feature_dir)\n        timings['feature_detection'].append(time() - t)\n        print(f'  - Detected features in {time()-t:.2f}s')\n        \n        # Match features\n        t = time()\n        match_superglue(images, pairs, feature_dir)\n        timings['feature_matching'].append(time() - t)\n        print(f'  - Matched features in {time()-t:.2f}s')\n        \n        # Geometric verification\n        t = time()\n        geometric_verification(images, pairs, feature_dir)\n        timings['geometric_verification'].append(time() - t)\n        print(f'  - Verified matches in {time()-t:.2f}s')\n        \n        # Create COLMAP database\n        if os.path.exists(database_path):\n            os.remove(database_path)\n        import_into_colmap(images_dir, feature_dir, database_path)\n         # Run reconstruction\n        t = time()\n        pycolmap.match_exhaustive(database_path)\n        mapper_options = pycolmap.IncrementalPipelineOptions()\n        mapper_options.min_model_size = 3\n        mapper_options.max_num_models = 25\n        os.makedirs(output_path, exist_ok=True)\n        maps = pycolmap.incremental_mapping(database_path, images_dir, output_path, mapper_options)\n        timings['reconstruction'].append(time() - t)\n        # Store results\n        registered = 0\n        filename_to_index = {p.filename: idx for idx, p in enumerate(predictions)}\n        for map_idx, cur_map in maps.items():\n            for _, image in cur_map.images.items():\n                idx = filename_to_index[image.name]\n                predictions[idx].cluster_index = map_idx\n                predictions[idx].rotation = image.cam_from_world.rotation.matrix()\n                predictions[idx].translation = image.cam_from_world.translation\n                registered += 1\n        \n        result = f'Dataset \"{dataset}\": Registered {registered}/{len(images)} images in {len(maps)} clusters'\n        results.append(result)\n        print(result)\n    except Exception as e:\n        result = f'Dataset \"{dataset}\": Failed - {str(e)}'\n        results.append(result)\n        print(result)\n\n\nprint('\\nSummary:')\nfor result in results:\n    print(result)\nprint('\\nTimings:')\nfor k, v in timings.items():\n    print(f'  - {k}: {sum(v):.2f}s')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-15T11:37:03.229405Z","iopub.status.idle":"2025-05-15T11:37:03.229700Z","shell.execute_reply.started":"2025-05-15T11:37:03.229583Z","shell.execute_reply":"2025-05-15T11:37:03.229597Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def array_to_str(array):\n    return ';'.join([f'{x:.9f}' for x in array])\n\ndef none_to_str(n):\n    return ';'.join(['nan'] * n)\n\nsubmission_file = '/kaggle/working/submission.csv'\nwith open(submission_file, 'w') as f:\n    header = 'image_id,dataset,scene,image,rotation_matrix,translation_vector\\n' if not is_train else \\\n             'dataset,scene,image,rotation_matrix,translation_vector\\n'\n    f.write(header)\n    for dataset in samples:\n        for pred in samples[dataset]:\n            cluster = 'outliers' if pred.cluster_index is None else f'cluster{pred.cluster_index}'\n            rot = none_to_str(9) if pred.rotation is None else array_to_str(pred.rotation.flatten())\n            trans = none_to_str(3) if pred.translation is None else array_to_str(pred.translation)\n            \n            if is_train:\n                f.write(f'{pred.dataset},{cluster},{pred.filename},{rot},{trans}\\n')\n            else:\n                f.write(f'{pred.image_id},{pred.dataset},{cluster},{pred.filename},{rot},{trans}\\n')\n\nprint(f'📄 Submission file created: {submission_file}')\n!head {submission_file}\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-15T11:37:03.230454Z","iopub.status.idle":"2025-05-15T11:37:03.230750Z","shell.execute_reply.started":"2025-05-15T11:37:03.230597Z","shell.execute_reply":"2025-05-15T11:37:03.230610Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if is_train:\n    t = time()\n    final_score, dataset_scores = metric.score(\n        gt_csv='/kaggle/input/image-matching-challenge-2025/train_labels.csv',\n        user_csv=submission_file,\n        thresholds_csv='/kaggle/input/image-matching-challenge-2025/train_thresholds.csv',\n        mask_csv=None,\n        inl_cf=0,\n        strict_cf=-1,\n        verbose=True\n    )\n    print(f'Evaluation score: {final_score:.3f}')\n    print(f'Evaluation time: {time()-t:.2f}s')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-15T11:37:03.231693Z","iopub.status.idle":"2025-05-15T11:37:03.231972Z","shell.execute_reply.started":"2025-05-15T11:37:03.231824Z","shell.execute_reply":"2025-05-15T11:37:03.231839Z"}},"outputs":[],"execution_count":null}]}