{"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.11.11"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":91498,"databundleVersionId":11655853,"sourceType":"competition"},{"sourceId":7884485,"sourceType":"datasetVersion","datasetId":4628051},{"sourceId":8469545,"sourceType":"datasetVersion","datasetId":4911029},{"sourceId":11843537,"sourceType":"datasetVersion","datasetId":7441260},{"sourceId":11972445,"sourceType":"datasetVersion","datasetId":7344616},{"sourceId":242190041,"sourceType":"kernelVersion"}],"dockerImageVersionId":31011,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":243.677717,"end_time":"2025-05-28T01:19:53.554266","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-05-28T01:15:49.876549","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"0ee77459","cell_type":"code","source":"!pip install --no-index /kaggle/input/imc2024-packages-lightglue-rerun-kornia/lightglue* --no-deps\n!pip install --no-index /kaggle/input/icm2025-packages/*.whl --no-deps\n!mkdir -p /root/.cache/torch/hub/checkpoints\n!cp /kaggle/input/imc2024-official-weights/* /root/.cache/torch/hub/checkpoints","metadata":{"execution":{"iopub.execute_input":"2025-05-28T01:15:53.953516Z","iopub.status.busy":"2025-05-28T01:15:53.953223Z","iopub.status.idle":"2025-05-28T01:16:05.170919Z","shell.execute_reply":"2025-05-28T01:16:05.170072Z"},"papermill":{"duration":11.222273,"end_time":"2025-05-28T01:16:05.172295","exception":false,"start_time":"2025-05-28T01:15:53.950022","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"c336c95f","cell_type":"code","source":"import os\nimport json\nfrom glob import glob\n\nINPUT_ROOT = \"/kaggle/input/image-matching-challenge-2025\"\nOUTPUT_ROOT = \"/kaggle/working/fnames\"\nos.makedirs(OUTPUT_ROOT, exist_ok=True)\n\nscenes = {}\nfor x in glob(os.path.join(INPUT_ROOT, \"test/*/*.*\")):\n    if \"LICENSE\" not in x:\n        folder = os.path.basename(os.path.dirname(x))\n        if folder not in scenes:\n            scenes[folder] = []\n        scenes[folder].append(x)\n\nsorted_keys = sorted(scenes.keys(), key=lambda k: len(scenes[k]))\n\n# Distribute scene keys to two workers\ntasks = [[] for _ in range(2)]\nfor idx, key in enumerate(sorted_keys):\n    worker_id = idx % 2\n    tasks[worker_id].append(key)\ntasks = [t for t in tasks if len(t) > 0]\n\n# Save to JSON files\nfor i, task in enumerate(tasks):\n    output_path = os.path.join(OUTPUT_ROOT, f\"worker_{i}_scenes.json\")\n    with open(output_path, \"w\") as f:\n        json.dump({s:scenes[s] for s in task}, f, indent=2)\n\nprint(f\"Saved scene splits to {OUTPUT_ROOT}\")\n","metadata":{"execution":{"iopub.execute_input":"2025-05-28T01:16:05.178617Z","iopub.status.busy":"2025-05-28T01:16:05.178019Z","iopub.status.idle":"2025-05-28T01:16:05.200147Z","shell.execute_reply":"2025-05-28T01:16:05.199531Z"},"papermill":{"duration":0.026243,"end_time":"2025-05-28T01:16:05.201239","exception":false,"start_time":"2025-05-28T01:16:05.174996","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"a33a9bdb","cell_type":"code","source":"%%writefile inference.py\nimport os\nimport json\nimport time\nimport sys\nimport networkx as nx\nimport pandas as pd\nimport numpy as np\nimport gc\nimport hdbscan\nimport argparse\nfrom math import ceil\nsys.path.append(\"/kaggle/input/image-matching-2025-code/image-matching-3D-main\")\n\nfrom images_to_3d.tasks.get_pair.get_image_pair_exhaustive import task_get_image_pair_exhaustive\nfrom images_to_3d.tasks.get_pair.get_image_pair_DINO import task_get_image_pair_DINO\nfrom images_to_3d.tasks.get_pair.get_image_pair_kNN import task_get_image_pair_kNN\nfrom images_to_3d.tasks.get_pair.get_transparent_pair import task_get_transparent_pair\nfrom images_to_3d.tasks.matching.matching import task_matching\nfrom images_to_3d.tasks.matching.matching_find_best import task_matching_find_best\nfrom images_to_3d.tasks.matching.rotate_matching_find_best import task_rotate_matching_find_best\nfrom images_to_3d.tasks.crop.sfm_mkpc import task_sfm_mkpc\nfrom images_to_3d.tasks.crop.pair_mkpc import task_pair_mkpc\nfrom images_to_3d.tasks.crop.transparent_crop import task_transparent_crop\nfrom images_to_3d.tasks.utils.ransac import task_ransac\nfrom images_to_3d.tasks.utils.concat import task_concat\nfrom images_to_3d.tasks.utils.rem_less_match_pair import task_rem_less_match_pair\nfrom images_to_3d.tasks.utils.count_matching_num import task_count_matching_num\nfrom images_to_3d.tasks.utils.extract_inliner_matching_points import task_extract_inliner_matching_points\nfrom images_to_3d.tasks.utils.extract_csv_pair import task_extract_csv_pair\nfrom images_to_3d.tasks.utils.estimate_rot import task_estimate_rot\nfrom images_to_3d.tasks.utils.get_exif import task_get_exif\nfrom images_to_3d.tasks.global_feat.get_image_feats import task_generate_image_feats\nfrom images_to_3d.models.utils import read_image, numpy_image_to_torch\nfrom images_to_3d.models import ALIKED\nfrom glob import glob\nfrom tqdm  import tqdm\nfrom pathlib import Path\nimport kornia as K\n\nINPUT_ROOT = \"/kaggle/input/image-matching-challenge-2025\"\nOUTPUT_ROOT = \"/kaggle/working/outputs\"\ntask_map = {\n    \"get_image_pair_exhaustive\": task_get_image_pair_exhaustive,\n    \"get_image_pair_DINO\": task_get_image_pair_DINO,\n    \"get_image_pair_kNN\": task_get_image_pair_kNN,\n    \"get_transparent_pair\": task_get_transparent_pair,\n\n    \"matching\": task_matching,\n    \"matching_find_best\": task_matching_find_best,\n    \"rotate_matching_find_best\": task_rotate_matching_find_best,\n\n    \"sfm_mkpc\": task_sfm_mkpc,\n    \"pair_mkpc\": task_pair_mkpc,\n    \"transparent_crop\": task_transparent_crop,\n    \n    \"ransac\": task_ransac,\n    \"concat\": task_concat,\n    \"rem_less_match_pair\": task_rem_less_match_pair,\n    \"count_matching_num\": task_count_matching_num,\n    \"extract_inliner_matching_points\": task_extract_inliner_matching_points,\n    \"extract_csv_pair\": task_extract_csv_pair,\n    \"estimate_rot\": task_estimate_rot,\n    \"get_exif\": task_get_exif,\n    \"generate_image_feats\": task_generate_image_feats,\n}\n\nimport json\npipleline = json.load(open(\"/kaggle/input/image-matching-2025-code/image-matching-3D-main/notebooks/pipeline.json\", \"r\"))\n\nfilter_threshold1 = 0.531049\npipleline[2][\"params\"][\"keypoint_matching_args\"][\"matcher_params\"][\"filter_threshold\"] = filter_threshold1\nfilter_threshold2 = 0.485205\npipleline[3][\"params\"][\"keypoint_matching_args\"][\"matcher_params\"][\"filter_threshold\"] = filter_threshold2\nfilter_threshold3 = 0.758147\npipleline[5][\"params\"][\"keypoint_matching_args\"][\"matcher_params\"][\"filter_threshold\"] = filter_threshold3\nfilter_threshold4 = 0.944721\npipleline[10][\"params\"][\"keypoint_matching_args\"][\"matcher_params\"][\"filter_threshold\"] = filter_threshold4\n\nth_matching_num = 33\npipleline[7]['params'][\"th_matching_num\"] = th_matching_num * 10\n\nransac_min_matches = 7\npipleline[-2]['params']['min_matches'] = ransac_min_matches * 10\n\npipleline[-2]['params']['ransac_params'][\"param1\"] = 2/2\npipleline[-2]['params']['ransac_params'][\"param2\"] = 0.767528\n\nclass Pipeline():\n    def __init__(self, data_dict, work_dir, input_dir_root, pipeline_config, device_id, pdb = False):\n        self.device = K.utils.get_cuda_device_if_available(device_id)\n        print(f\"device: {self.device}\")\n        self.data_dict = data_dict\n        self.work_dir = work_dir\n        self.input_dir_root = input_dir_root\n        os.makedirs(self.work_dir, exist_ok=True)\n        self.pipeline_config = pipeline_config\n        self.processing_times = {\n            \"task\": [],\n            \"comment\": [],\n            \"processing_time\": []\n        }\n        self.pdb = pdb\n\n\n    def exec(self):\n        all_processing_time = 0\n        for p in self.pipeline_config:\n            task = p[\"task\"]\n            comment = p[\"comment\"]\n            p[\"params\"][\"device\"] = self.device\n            p[\"params\"][\"data_dict\"] = self.data_dict\n            p[\"params\"][\"work_dir\"] = self.work_dir\n            p[\"params\"][\"input_dir_root\"] = self.input_dir_root\n            p[\"params\"][\"pdb\"] = self.pdb\n            \n            start = time.time()\n            print(f\"===== [{task}] {comment} =====\")\n            task_map[task](p[\"params\"])\n            gc.collect()\n            print(\"====================\")\n            end = time.time()\n\n            self.processing_times[\"task\"].append(task)\n            self.processing_times[\"comment\"].append(comment)\n            self.processing_times[\"processing_time\"].append(end-start)\n            all_processing_time += end-start\n        \n        self.processing_times[\"task\"].append(\"All\")\n        self.processing_times[\"comment\"].append(\"\")\n        self.processing_times[\"processing_time\"].append(all_processing_time)\n        processing_times_df = pd.DataFrame.from_dict(self.processing_times)\n        processing_times_df.to_csv(os.path.join(self.work_dir, \"processing_time.csv\"), index=False)\n\nfrom collections import defaultdict\nimport shutil\nfrom images_to_3d.tasks.reconstruction.reconstruct import reconstruction\ndef find_connected_components(edges, minimum_size=9):\n    # Build the adjacency list\n    graph = defaultdict(set)\n    nodes = set()\n    for u, v in edges:\n        graph[u].add(v)\n        graph[v].add(u)\n        nodes.update([u, v])\n    \n    visited = set()\n    components = []\n\n    def dfs(node, component):\n        visited.add(node)\n        component.append(node)\n        for neighbor in graph[node]:\n            if neighbor not in visited:\n                dfs(neighbor, component)\n\n    for node in nodes:\n        if node not in visited:\n            component = []\n            dfs(node, component)\n            components.append(component)\n    #post processing\n    components = [c for c in components if len(c)>=minimum_size]\n    return len(components), components\n\ndef get_components(edges,minimum_size=5):\n    G = nx.Graph()\n    G.add_edges_from(edges)\n    num_degrees = list(dict(G.degree()).values())\n    \n    pos = nx.spring_layout(G, k=0.5, iterations=100, seed=42)\n    model = hdbscan.HDBSCAN(min_cluster_size=5,)\n    labels = model.fit_predict(np.stack(list(pos.values())))   \n\n    degree_counts = {x: [] for x in np.unique(labels)}\n    for d,l in zip(num_degrees,labels):\n        degree_counts[l].append(d)\n    \n    connectedness = np.mean([np.mean(v)/len(v) for k,v in degree_counts.items() if k!=-1])\n    connectedness = np.nan_to_num(connectedness,posinf=0,neginf=0,nan=0)\n    if model.probabilities_.mean()>0.8 and connectedness>0.5:\n        print(\"using hdbscan\")\n        c = np.unique(labels[labels!=-1])\n        connected_components = {idx:[] for idx in c}\n        for node_label, position in zip(labels, pos.keys()):\n            if node_label in connected_components:\n                connected_components[node_label].append(position)\n        connected_components = [v for v in connected_components.values() if len(v)>0]\n        return len(connected_components),connected_components\n    else :\n        print(\"using spanning tree\")\n        c,connected_components = find_connected_components(edges, minimum_size=minimum_size)\n    \n    return c,connected_components\n        \nclass Config: \n    min_size=5\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser()\n    parser.add_argument('--world_size', type=int, required=True)\n    parser.add_argument('--rank', type=int, required=True)\n    args = parser.parse_args()\n\n    scenes = json.load(open(os.path.join(\"/kaggle/working/fnames\", f\"worker_{args.rank}_scenes.json\")))\n    scenes = {k:[Path(p) for p in v] for k,v in scenes.items()}\n    gpu_id = os.environ[\"CUDA_VISIBLE_DEVICES\"]\n    print(f\"running {args.rank}/ {args.world_size} with gpu {gpu_id} : {scenes.keys()}\")\n    for dset_name in scenes:\n        try:\n            work_dir = os.path.join(OUTPUT_ROOT, dset_name)\n            pipeline = Pipeline(scenes[dset_name], Path(work_dir), Path(INPUT_ROOT), pipleline, 0, pdb = False)\n            pipeline.exec()\n            print(f\"Processing {work_dir}\")\n            print(\"=\"*100)\n            image_pairs = pd.read_csv(os.path.join(work_dir, \"image_pair.csv\"))\n            counts, connected_components = get_components([(a,b) for a,b in zip(image_pairs.key1, image_pairs.key2)], minimum_size=Config.min_size)\n            print(f\"Found {counts} connected components of length {[len(c) for c in connected_components]}\")\n            \n            colmap_mapper_options = {\n                \"min_model_size\": 3, # By default colmap does not generate a reconstruction if less than 10 images are registered. Lower it to 3.\n                \"max_num_models\": 25,\n                #\"num_threads\": 1,\n            }\n            \n            for i,image_paths in enumerate(connected_components):\n                output_dir = os.path.join(work_dir, f\"minsize_{Config.min_size}_scene_{i}\")\n                os.makedirs(output_dir, exist_ok=True)\n                image_root = str(scenes[os.path.basename(work_dir)][0].parents[0])\n                reconstruction(image_root, image_paths, os.path.basename(work_dir), f\"minsize_{Config.min_size}_scene_{i}\", work_dir, output_dir,colmap_mapper_options,image_model=\"radial\")\n                print(f\"Scene {i} done\")    \n                print(\"=\"*100)\n        except:\n            pass","metadata":{"execution":{"iopub.execute_input":"2025-05-28T01:16:05.206735Z","iopub.status.busy":"2025-05-28T01:16:05.206553Z","iopub.status.idle":"2025-05-28T01:16:05.214776Z","shell.execute_reply":"2025-05-28T01:16:05.214087Z"},"papermill":{"duration":0.01239,"end_time":"2025-05-28T01:16:05.215885","exception":false,"start_time":"2025-05-28T01:16:05.203495","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"639590ce","cell_type":"code","source":"import subprocess\nimport os\n# Process for rank 0 on GPU 0\np0 = subprocess.Popen(\n    ['python3', 'inference.py', '--world_size=2', '--rank=0'],\n    env={**os.environ, 'CUDA_VISIBLE_DEVICES': '0'}\n)\n\n# Process for rank 1 on GPU 1\np1 = subprocess.Popen(\n    ['python3', 'inference.py', '--world_size=2', '--rank=1'],\n    env={**os.environ, 'CUDA_VISIBLE_DEVICES': '1'}\n)\n\n# Wait for both to complete (optional)\np0.wait()\np1.wait()\nprint(\"Both inference processes have completed.\")","metadata":{"execution":{"iopub.execute_input":"2025-05-28T01:16:05.221449Z","iopub.status.busy":"2025-05-28T01:16:05.221012Z","iopub.status.idle":"2025-05-28T01:19:52.768819Z","shell.execute_reply":"2025-05-28T01:19:52.767910Z"},"papermill":{"duration":227.551964,"end_time":"2025-05-28T01:19:52.770126","exception":false,"start_time":"2025-05-28T01:16:05.218162","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"a44bfe40","cell_type":"code","source":"import os\nimport time\nimport sys\nimport pandas as pd\nimport numpy as np\nfrom glob import glob\nfrom pathlib import Path\nimport gc\n\nINPUT_ROOT = \"/kaggle/input/image-matching-challenge-2025\"\nOUTPUT_ROOT = \"/kaggle/working/outputs\"\nscenes = {}\nfor x in glob(os.path.join(INPUT_ROOT, \"test/*/*.*\")):\n    if \"LICENSE\" not in x:\n        folder = os.path.basename(os.path.dirname(x))\n        if folder not in scenes:\n            scenes[folder] = []\n        scenes[folder].append(Path(x))\nUSED_COLS = [\"image_id\", \"dataset\", \"scene\", \"image\" , \"rotation_matrix\", \"translation_vector\"]\n\nsubmission_images = pd.concat([pd.read_csv(x) for x in glob(os.path.join(OUTPUT_ROOT, \"*\",\"minsize_5_scene_*\",\"submission.csv\"))])\nsubmission_images[\"dataset\"] = submission_images[\"image_path\"].apply(lambda x: x.split(\"/\")[5])\nsubmission_images[\"image\"] = submission_images[\"image_path\"].apply(lambda x: os.path.basename(x))\nsubmission_images[\"image_id\"] = submission_images[\"dataset\"] +  \"_\" + submission_images[\"image\"] + \"_public\"\nsubmission_images = submission_images[USED_COLS]\n\nnon_outlier_ids = set(submission_images[\"image_id\"])\n\noutliers_data = []\nfor dset_name, images in scenes.items():\n    for img_path in images:\n        filename = str(img_path).split(\"/\")[-1]\n        img_id = dset_name + \"_\" + filename + \"_public\"\n        if img_id in non_outlier_ids:\n            continue\n        new_row = {\n            \"image_id\": img_id,\n            \"dataset\": dset_name,\n            \"scene\": \"outliers\",\n            \"image\": filename,\n            \"rotation_matrix\": 'nan;nan;nan;nan;nan;nan;nan;nan;nan', \n            \"translation_vector\": 'nan;nan;nan'               \n        }\n        outliers_data.append(new_row)\nsubmission_images = pd.concat([submission_images,pd.DataFrame(outliers_data)])\n\nsubmission_images.sort_values([\"dataset\",\"scene\"]).to_csv(\"submission.csv\",index=False)","metadata":{"execution":{"iopub.execute_input":"2025-05-28T01:19:52.858084Z","iopub.status.busy":"2025-05-28T01:19:52.857807Z","iopub.status.idle":"2025-05-28T01:19:53.146656Z","shell.execute_reply":"2025-05-28T01:19:53.146052Z"},"papermill":{"duration":0.334336,"end_time":"2025-05-28T01:19:53.147951","exception":false,"start_time":"2025-05-28T01:19:52.813615","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}