{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":91498,"databundleVersionId":11655853,"sourceType":"competition"},{"sourceId":11924468,"sourceType":"datasetVersion","datasetId":6988459},{"sourceId":289561907,"sourceType":"kernelVersion"}],"dockerImageVersionId":30918,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"# **IMC2025 3D Reconstruction w/ match_vocab_tree**\n","metadata":{},"attachments":{}},{"cell_type":"markdown","source":"## Complete Notebook Explanation: Image Matching Challenge 2025\n\nThis notebook implements a **Structure-from-Motion (SfM) pipeline** for the Image Matching Challenge 2025. The goal is to estimate camera poses (rotation and translation) for images across multiple datasets.\n\n---\n\n## 📦 **1. Setup & Installation**\n\n```python\nimport pycolmap\nprint(f\"pycolmap version: {pycolmap.__version__}\")\n```\n\n**Purpose:** Installs and imports `pycolmap` (version 0.6.1), the Python binding for COLMAP - a powerful SfM library.\n\n**Key Dependencies:**\n- `pycolmap`: Core SfM functionality\n- `numpy`, `pandas`: Data manipulation\n- `h5py`: Feature storage\n- `networkx`: Graph operations\n- `PIL`: Image handling\n- Custom modules: `database`, `h5_to_db`, `metric`\n\n---\n\n## ⚙️ **2. Configuration**\n\n```python\nclass CONFIG:\n    vocab_tree_path = '/kaggle/input/download-pycolmap/vocab_tree_flickr100K_words256K.bin'\n    num_images = 100\n    num_pallalel_sfm = 4\n```\n\n**Parameters:**\n- `vocab_tree_path`: Pre-trained visual vocabulary for efficient image pair proposal\n- `num_images`: Batch size for vocab tree matcher (100 images at a time)\n- `num_pallalel_sfm`: Number of parallel SfM processes (4 datasets simultaneously)\n\n---\n\n## 🗂️ **3. Data Structures**\n\n### **Prediction Class**\n```python\n@dataclasses.dataclass\nclass Prediction:\n    image_id: str | None          # Unique ID (test only)\n    dataset: str                   # Dataset name\n    filename: str                  # Image filename\n    map_size: int = -1            # Size of 3D model that registered this image\n    cluster_index: int | None      # Which 3D model (cluster) this belongs to\n    retry_index: int = -1         # Retry attempt number\n    rotation: np.ndarray | None    # 3x3 rotation matrix\n    translation: np.ndarray | None # 3x1 translation vector\n```\n\n**Purpose:** Stores prediction results for each image. Initially empty, populated after reconstruction.\n\n### **DatasetParams Class**\n```python\n@dataclasses.dataclass\nclass DatasetParams:\n    dataset: str\n    feature_dir: str | None\n    images_dir: str | None\n    filename_to_index: dict | None\n    predictions: list | None\n    images: list | None\n    laptime_feature_extraction: float = -1.0\n    laptime_matching: float = -1.0\n    laptime_sfm: float = -1.0\n    list_model_size: list | None\n```\n\n**Purpose:** Container for all dataset-specific information and timing metrics.\n\n---\n\n## 🔧 **4. COLMAP Core Functions**\n\n### **4.1 Feature Extraction**\n\n```python\ndef extract_features_colmap(database_path, image_path):\n    sift_options = pycolmap.SiftExtractionOptions()\n    sift_options.max_num_features = 8192  # Extract up to 8192 SIFT features per image\n    \n    camera_mode = pycolmap.CameraMode.SINGLE  # All images share same camera\n    \n    pycolmap.extract_features(\n        database_path=database_path,\n        image_path=image_path,\n        camera_mode=camera_mode,\n        sift_options=sift_options\n    )\n```\n\n**What it does:**\n1. Detects **SIFT keypoints** in each image (corners, blobs, edges)\n2. Computes **128-dimensional descriptors** for each keypoint\n3. Stores features in COLMAP database\n4. Assumes all images from same camera (shared intrinsics)\n\n**Why 8192 features?** Balance between:\n- More features → Better matching accuracy\n- Fewer features → Faster processing\n\n---\n\n### **4.2 Vocabulary Tree Matcher**\n\n```python\ndef vocab_tree_matcher(database_path, vocab_tree_path):\n    matching_options = pycolmap.SiftMatchingOptions()\n    \n    pycolmap.match_vocab_tree(\n        database_path=database_path,\n        vocab_tree_path=str(vocab_tree_path),\n        sift_options=matching_options\n    )\n```\n\n**How it works:**\n\n1. **Visual Vocabulary**: Pre-trained tree of 256K \"visual words\" (clusters of SIFT descriptors)\n2. **Quantization**: Each image's features → mapped to visual words\n3. **Inverted Index**: Creates index of which images contain which words\n4. **Scoring**: Computes similarity scores between all image pairs\n5. **Pair Proposal**: Suggests most promising pairs for matching\n\n**Key Advantage:** \n- Without vocab tree: Match all pairs = O(n²) comparisons (e.g., 1000 images = 500K pairs)\n- With vocab tree: Match only promising pairs = O(n) comparisons (e.g., 1000 images = ~10K pairs)\n\n**⚠️ Critical Note:** This only **proposes pairs**, doesn't establish actual feature correspondences!\n\n---\n\n### **4.3 Exhaustive Matcher (Fallback)**\n\n```python\ndef exhaustive_matcher(database_path):\n    matching_options = pycolmap.SiftMatchingOptions()\n    pycolmap.match_exhaustive(\n        database_path=database_path,\n        sift_options=matching_options\n    )\n```\n\n**When used:** Small datasets where vocab tree overhead isn't worth it.\n\n**What it does:** Tries to match every image with every other image.\n\n---\n\n### **4.4 Incremental Mapping (3D Reconstruction)**\n\n```python\ndef incremental_mapping(database_path, image_path, output_path):\n    output_path.mkdir(parents=True, exist_ok=True)\n    mapper_options = pycolmap.IncrementalMapperOptions()\n    \n    maps = pycolmap.incremental_mapping(\n        database_path=database_path,\n        image_path=image_path,\n        output_path=output_path,\n        options=mapper_options\n    )\n    \n    return maps\n```\n\n**The Real Magic Happens Here:**\n\n**Step-by-step process:**\n\n1. **Initialize:** Start with best image pair (most matches, good geometry)\n2. **Triangulation:** Create initial 3D points from the pair\n3. **Image Registration:** \n   - For each remaining image, find 2D-3D correspondences\n   - Solve PnP (Perspective-n-Point) to get camera pose\n   - Add image to model if successful\n4. **Triangulation:** Add new 3D points from newly registered images\n5. **Bundle Adjustment:** Refine all cameras and 3D points jointly\n6. **Repeat:** Until no more images can be registered\n\n**Output:** Dictionary of reconstructed 3D models (can be multiple disconnected scenes)\n\n---\n\n## 🔄 **5. Pipeline Functions**\n\n### **5.1 Feature Extraction + Matching Pipeline**\n\n```python\ndef exec_feature_extraction_and_matching(params):\n    database_path = Path(params.feature_dir) / \"colmap.db\"\n    images_dir = Path(params.images_dir)\n    \n    # 1. Extract features\n    print(f\"[{params.dataset}] Feature extraction\")\n    t = time()\n    extract_features_colmap(database_path, images_dir)\n    params.laptime_feature_extraction = time() - t\n    \n    # 2. Propose pairs (NOT actual matching!)\n    print(f\"[{params.dataset}] Pair proposal\")\n    t = time()\n    if use_vocab_tree and vocab_tree_path:\n        pycolmap.match_vocab_tree(\n            database_path=database_path,\n            vocab_tree_path=str(vocab_tree_path),\n            max_num_matches=50,      # Top 50 similar images per query\n            guided_matching=False     # Don't do actual matching\n        )\n    else:\n        exhaustive_matcher(database_path)\n    \n    params.laptime_matching = time() - t\n    return params\n```\n\n**Why `guided_matching=False`?**\n- We only want pair proposals here\n- Actual geometric matching happens during incremental_mapping\n- This saves computation time\n\n---\n\n### **5.2 Reconstruction Pipeline**\n\n```python\ndef exec_reconstruction(params):\n    database_path = Path(params.feature_dir) / \"colmap.db\"\n    images_dir = Path(params.images_dir)\n    output_path = Path(params.feature_dir) / \"sparse\"\n    \n    maps = pycolmap.incremental_mapping(\n        database_path=database_path,\n        image_path=images_dir,\n        output_path=output_path,\n    )\n    \n    params.maps = maps\n    params.sparse_output_path = output_path\n    return params\n```\n\n**Output:** One or more 3D models, each containing:\n- Registered images with camera poses\n- 3D point cloud\n- 2D-3D correspondences\n\n---\n\n### **5.3 Extract Predictions from Models**\n\n```python\ndef update_predictions_from_maps(params, maps):\n    filename_to_index = params.filename_to_index\n    predictions = params.predictions\n    \n    # Sort models by size (number of registered images)\n    list_num_images = [len(rec.images) for rec in maps.values()]\n    sort_idx = np.argsort(list_num_images)\n    \n    # Extract poses from each model\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        \n        if cur_map_size < 3:  # Skip tiny models\n            continue\n        \n        for index, image in cur_map.images.items():\n            prediction_index = filename_to_index[image.name]\n            \n            # Keep pose from largest model only\n            if cur_map_size > predictions[prediction_index].map_size:\n                predictions[prediction_index].map_size = cur_map_size\n                predictions[prediction_index].cluster_index = map_index\n                predictions[prediction_index].rotation = image.cam_from_world.rotation.matrix()\n                predictions[prediction_index].translation = image.cam_from_world.translation\n```\n\n**Logic:**\n- If an image appears in multiple models, use the **largest model's pose**\n- Larger models are generally more reliable (more constraints)\n- Models with <3 images are considered unreliable\n\n---\n\n### **5.4 Complete Dataset Processing**\n\n```python\ndef process_dataset(params):\n    # Step 1: Features + Pair proposal\n    params = exec_feature_extraction_and_matching(params)\n    \n    # Step 2: 3D reconstruction\n    params = exec_reconstruction(params)\n    \n    return params\n```\n\n**Simple wrapper** that chains the entire pipeline for one dataset.\n\n---\n\n## 🚀 **6. Main Execution**\n\n### **6.1 Setup Paths**\n\n```python\nis_train = False  # Set to True for training data\ndata_dir = '/kaggle/input/image-matching-challenge-2025'\nworkdir = '/tmp/kaggle/working/result/'\n\nif is_train:\n    sample_submission_csv = os.path.join(data_dir, 'train_labels.csv')\n    base_dir = os.path.join(data_dir, 'train')\nelse:\n    sample_submission_csv = os.path.join(data_dir, 'sample_submission.csv')\n    base_dir = os.path.join(data_dir, 'test')\n```\n\n**Modes:**\n- **Training mode:** Has ground truth labels, can compute score\n- **Test mode:** No labels, generates submission for leaderboard\n\n---\n\n### **6.2 Load Competition Data**\n\n```python\nsamples = {}\ncompetition_data = pd.read_csv(sample_submission_csv)\n\nfor _, row in competition_data.iterrows():\n    if row.dataset not in samples:\n        samples[row.dataset] = []\n    \n    prediction = Prediction(\n        image_id=None if is_train else row.image_id,\n        dataset=row.dataset,\n        filename=row.image\n    )\n    samples[row.dataset].append(prediction)\n```\n\n**Creates initial predictions** for all images (poses are None initially).\n\n**Sample CSV structure:**\n```\nimage_id,dataset,image\n1,ETs,image_001.jpg\n2,ETs,image_002.jpg\n3,stairs,stair_01.png\n...\n```\n\n---\n\n### **6.3 Filter Existing Datasets**\n\n```python\nexisting_datasets = {}\nfor dataset in samples:\n    dataset_path = os.path.join(base_dir, dataset)\n    \n    if os.path.exists(dataset_path) and os.path.isdir(dataset_path):\n        image_files = [f for f in os.listdir(dataset_path) \n                      if f.endswith(('.jpg', '.jpeg', '.png', ...))]\n        \n        if len(image_files) > 0:\n            existing_datasets[dataset] = samples[dataset]\n            print(f'✅ {dataset} -> {len(samples[dataset])} images')\n        else:\n            print(f'⚠️  {dataset} -> No images found')\n    else:\n        print(f'❌ {dataset} -> Directory does not exist')\n\nsamples = existing_datasets\n```\n\n**Why filter?**\n- Competition data lists 13 datasets\n- Kaggle environment only has 2 available (ETs, stairs)\n- Prevents errors from trying to process non-existent data\n\n**Output example:**\n```\n✅ ETs                            -> 22 images (found 22 files)\n❌ amy_gardens                    -> Directory does not exist (skipping)\n✅ stairs                         -> 51 images (found 51 files)\n```\n\n---\n\n### **6.4 Parallel Processing**\n\n```python\nwith concurrent.futures.ThreadPoolExecutor(max_workers=CONFIG.num_pallalel_sfm) as executor:\n    futures = {}\n    \n    for dataset, predictions in samples.items():\n        images_dir = os.path.join(base_dir, dataset)\n        feature_dir = os.path.join(workdir, 'featureout', dataset)\n        os.makedirs(feature_dir, exist_ok=True)\n        \n        filename_to_index = {p.filename: idx for idx, p in enumerate(predictions)}\n        \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=existing_images,\n        )\n        \n        # Submit to thread pool\n        futures[dataset] = executor.submit(process_dataset, dataset_params)\n    \n    # Collect results\n    for dataset in samples.keys():\n        try:\n            dataset_params = futures[dataset].result()\n            samples[dataset] = dataset_params.predictions  # Update with poses\n            print(f'✅ Dataset \"{dataset}\": Processing complete')\n        except Exception as e:\n            print(f'❌ Dataset \"{dataset}\": Error - {e}')\n```\n\n**Parallel Execution:**\n- Processes up to 4 datasets simultaneously\n- Each dataset runs: feature extraction → matching → reconstruction\n- Results collected as they complete\n\n**Actual output:**\n```\nProcessing: Dataset \"ETs\": 22 images\n[ETs] Feature extraction\n[ETs] Pair proposal\nRunning 3D reconstruction for ETs...\nReconstruction done in 2.4730 sec\nGenerated 1 3D models\n✅ Dataset \"ETs\": Processing complete\n```\n\n---\n\n## 📝 **7. Create Submission File**\n\n```python\narray_to_str = lambda array: ';'.join([f\"{x:.09f}\" for x in array])\nnone_to_str = lambda n: ';'.join(['nan'] * n)\n\nwith open(submission_file, 'w') as f:\n    if is_train:\n        f.write('dataset,scene,image,rotation_matrix,translation_vector\\n')\n    else:\n        f.write('image_id,dataset,scene,image,rotation_matrix,translation_vector\\n')\n    \n    for dataset in samples:\n        for prediction in samples[dataset]:\n            cluster_name = 'outliers' if prediction.cluster_index is None else f'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            \n            if is_train:\n                f.write(f'{prediction.dataset},{cluster_name},{prediction.filename},{rotation},{translation}\\n')\n            else:\n                f.write(f'{prediction.image_id},{prediction.dataset},{cluster_name},{prediction.filename},{rotation},{translation}\\n')\n```\n\n**Submission Format:**\n\n**Test mode:**\n```\nimage_id,dataset,scene,image,rotation_matrix,translation_vector\n1,ETs,cluster0,img1.jpg,0.999;-0.012;0.034;0.013;0.999;-0.045;-0.033;0.045;0.998,1.234;-0.567;2.890\n2,ETs,outliers,img2.jpg,nan;nan;nan;nan;nan;nan;nan;nan;nan,nan;nan;nan\n```\n\n**Format details:**\n- `rotation_matrix`: 9 values (3×3 matrix, row-major order), semicolon-separated\n- `translation_vector`: 3 values (x, y, z), semicolon-separated\n- `scene`: `cluster0`, `cluster1`, ... or `outliers`\n- `nan` for images that failed to register\n\n---\n\n## 📊 **8. Summary & Scoring**\n\n### **8.1 Display Timing Summary**\n\n```python\nfor dataset_params in dataset_logs:\n    dataset_params.summary()\n```\n\n**Output:**\n```\n====================================================================================================\n[Summary]\n- Dataset                   : ETs\n- images                    : 22\n- laptime_feature_extraction: 6.11\n- laptime_matching          : 12.14\n- laptime_sfm               : -1.00\n====================================================================================================\n```\n\n### **8.2 Compute Score (Training Only)**\n\n```python\nif is_train:\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```\n\n**Scoring metrics** (typical for pose estimation):\n- **Rotation error**: Angular difference between predicted and ground truth rotation\n- **Translation error**: Distance between predicted and ground truth translation\n- **Thresholds**: Different accuracy levels (strict, moderate, loose)\n\n---\n\n## 🎯 **Complete Pipeline Flow**\n\n```\n1. Load CSV → Create empty Predictions\n                ↓\n2. Check which datasets exist on disk\n                ↓\n3. For each dataset (in parallel):\n   a. Extract SIFT features (8192 per image)\n   b. Vocab tree → Propose similar image pairs\n   c. Incremental SfM:\n      - Match features for proposed pairs\n      - Geometric verification (RANSAC)\n      - Build 3D model incrementally\n      - Bundle adjustment\n   d. Extract camera poses from models\n                ↓\n4. Write submission CSV:\n   - Registered images → rotation + translation\n   - Unregistered images → nan values\n                ↓\n5. (If training) Compute score vs ground truth\n```\n\n---\n\n## 🔑 **Key Design Decisions**\n\n1. **Vocabulary Tree for Scalability**\n   - Essential for large datasets (100s-1000s of images)\n   - Reduces matching from O(n²) to O(n)\n\n2. **Parallel Processing**\n   - Independent datasets processed simultaneously\n   - 4 workers → ~4x speedup\n\n3. **Prioritize Large Models**\n   - Images in larger reconstructions are more reliable\n   - Small models (<3 images) likely spurious\n\n4. **Graceful Failure Handling**\n   - Missing datasets → skip\n   - Failed reconstructions → output nan\n   - Allows partial submissions\n\n5. **No subprocess Calls**\n   - Uses `pycolmap` Python API directly\n   - More reliable, easier error handling\n   - Better integration with Python workflow\n\n---\n\n## 📈 **Performance Notes**\n\nFrom the output:\n- **ETs dataset (22 images):**\n  - Feature extraction: 6.11s\n  - Pair proposal: 12.14s\n  - Reconstruction: 2.47s\n  - Total: ~20s\n\n- **stairs dataset (51 images):**\n  - Feature extraction: 66.22s\n  - Pair proposal: 120.90s\n  - Reconstruction: 0.43s\n  - Total: ~3 minutes\n\n**Bottlenecks:** Pair proposal is slowest (vocab tree overhead), but scales much better than alternatives for large datasets.\n\n---\n\nThis is a production-ready SfM pipeline suitable for the Image Matching Challenge, with robust error handling, parallel processing, and efficient matching strategies! 🚀","metadata":{}},{"cell_type":"code","source":"import sys\nimport subprocess\n\nwheel_path = '/kaggle/input/download-pycolmap/pycolmap_wheels/'\n\nsubprocess.check_call([\n    sys.executable, '-m', 'pip', 'install', \n    '--find-links', wheel_path, \n    'pycolmap'\n])\n\nimport pycolmap\nprint(f\"pycolmap version: {pycolmap.__version__}\")\nprint(dir(pycolmap))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Version using only COLMAP vocab_tree_matcher\n\nimport 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 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\nimport subprocess\n\n#import pycolmap\nsys.path.append('/kaggle/input/imc25-utils')\nfrom database import *\nfrom h5_to_db import *\nimport metric\n\nimport concurrent.futures\n\n# Install COLMAP\n#!cd /kaggle/input/pkg-colmap/colmap_offline && dpkg -i ./*.deb\n\n# =========================================================\n# Configuration\n# =========================================================\nclass CONFIG:\n    # vocab_tree_matcher settings\n    vocab_tree_path = '/kaggle/input/download-pycolmap/vocab_tree_flickr100K_words256K.bin'\n    num_images = 100  # Number of images vocab_tree_matcher processes at once\n    # SfM settings\n    num_pallalel_sfm = 4\n\n# =========================================================\n# Data Structures\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@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    laptime_feature_extraction: float = -1.0\n    laptime_matching: float = -1.0\n    laptime_sfm: float = -1.0\n    list_model_size: 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        print(f\"- laptime_feature_extraction: {self.laptime_feature_extraction:.2f}\")\n        print(f\"- laptime_matching          : {self.laptime_matching:.2f}\")\n        print(f\"- laptime_sfm               : {self.laptime_sfm:.2f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pycolmap\nfrom pathlib import Path\n\n# =========================================================\n# COLMAP Feature Extraction & Matching (Fully Refactored)\n# =========================================================\n\ndef extract_features_colmap(database_path, image_path):\n    \"\"\"\n    Feature extraction using the pycolmap Python API.\n    Does not use subprocess.\n    \"\"\"\n    database_path = Path(database_path)\n    image_path = Path(image_path)\n    \n    # Configure SIFT options\n    sift_options = pycolmap.SiftExtractionOptions()\n    sift_options.max_num_features = 8192\n    \n    # Camera mode: SINGLE (All images share the same camera parameters)\n    camera_mode = pycolmap.CameraMode.SINGLE\n    \n    # Execute feature extraction\n    pycolmap.extract_features(\n        database_path=database_path,\n        image_path=image_path,\n        camera_mode=camera_mode,\n        sift_options=sift_options\n    )\n\n\ndef vocab_tree_matcher(database_path, vocab_tree_path):\n    \"\"\"\n    Execute Vocabulary Tree matching.\n    Does not use subprocess.\n    \"\"\"\n    database_path = Path(database_path)\n    vocab_tree_path = Path(vocab_tree_path)\n    \n    # Configure matching options\n    matching_options = pycolmap.SiftMatchingOptions()\n    \n    # Execute Vocab tree matching\n    pycolmap.match_vocab_tree(\n        database_path=database_path,\n        vocab_tree_path=str(vocab_tree_path),\n        sift_options=matching_options\n    )\n\n\ndef exhaustive_matcher(database_path):\n    \"\"\"\n    Exhaustive matcher (for small to medium datasets).\n    Does not use subprocess.\n    \"\"\"\n    database_path = Path(database_path)\n    \n    # Configure matching options\n    matching_options = pycolmap.SiftMatchingOptions()\n    \n    # Execute Exhaustive matching\n    pycolmap.match_exhaustive(\n        database_path=database_path,\n        sift_options=matching_options\n    )\n\n\ndef incremental_mapping(database_path, image_path, output_path):\n    \"\"\"\n    Execute 3D reconstruction (Structure from Motion).\n    Does not use subprocess.\n    \"\"\"\n    database_path = Path(database_path)\n    image_path = Path(image_path)\n    output_path = Path(output_path)\n    \n    # Create output directory\n    output_path.mkdir(parents=True, exist_ok=True)\n    \n    # Configure mapper options\n    mapper_options = pycolmap.IncrementalMapperOptions()\n    \n    # Execute reconstruction\n    maps = pycolmap.incremental_mapping(\n        database_path=database_path,\n        image_path=image_path,\n        output_path=output_path,\n        options=mapper_options\n    )\n    \n    return maps","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"NOTE: vocab_tree is used ONLY for pair proposal.","metadata":{}},{"cell_type":"code","source":"def exec_feature_extraction_and_matching(params):\n    \"\"\"\n    Feature extraction + pair proposal pipeline.\n    NOTE: vocab_tree is used ONLY for pair proposal.\n    \"\"\"\n    database_path = Path(params.feature_dir) / \"colmap.db\"\n    images_dir = Path(params.images_dir)\n\n    vocab_tree_path = getattr(CONFIG, 'vocab_tree_path', None)\n    use_vocab_tree = getattr(CONFIG, 'use_vocab_tree', False)\n\n    from time import time\n\n    # 1. Feature Extraction\n    print(f\"[{params.dataset}] Feature extraction\")\n    t = time()\n    extract_features_colmap(database_path, images_dir)\n    params.laptime_feature_extraction = time() - t\n\n    # 2. Pair proposal only\n    print(f\"[{params.dataset}] Pair proposal\")\n    t = time()\n\n    if use_vocab_tree and vocab_tree_path and Path(vocab_tree_path).exists():\n        # IMPORTANT:\n        # Do NOT trust matching here\n        pycolmap.match_vocab_tree(\n            database_path=database_path,\n            vocab_tree_path=str(vocab_tree_path),\n            max_num_matches=50,   \n            guided_matching=False\n        )\n    else:\n        # small dataset fallback\n        exhaustive_matcher(database_path)\n\n    params.laptime_matching = time() - t\n    return params\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_dataset(params):\n    \"\"\"\n    Full pipeline for processing the dataset.\n    \"\"\"\n    # Feature Extraction & Matching\n    params = exec_feature_extraction_and_matching(params)\n    \n    # 3D Reconstruction\n    print(f\"Running 3D reconstruction for {params.dataset}...\")\n    from time import time\n    t = time()\n    \n    # Set database and output paths\n    database_path = Path(params.feature_dir) / \"colmap.db\"\n    output_path = Path(params.feature_dir) / \"sparse\"\n    \n    maps = incremental_mapping(\n        database_path=database_path,\n        image_path=params.images_dir,\n        output_path=output_path\n    )\n    \n    params.laptime_reconstruction = time() - t\n    print(f'Reconstruction completed in {params.laptime_reconstruction:.4f} sec')\n    print(f'Generated {len(maps)} 3D models')\n    \n    # Save reconstruction results to params\n    params.maps = maps\n    params.sparse_output_path = output_path\n    \n    return params\n\n\n# =========================================================\n# IMPORTANT: Remove all subprocess.run() calls\n# =========================================================\n# Delete any code like the following:\n# subprocess.run(['colmap', ...], check=True)  # <-- DELETE THIS","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# Reconstruction\n# =========================================================\ndef exec_reconstruction(params):\n\n    from time import time\n\n    database_path = Path(params.feature_dir) / \"colmap.db\"\n    images_dir = Path(params.images_dir)\n    output_path = Path(params.feature_dir) / \"sparse\"\n    \n    print(f\"Running 3D reconstruction for {params.dataset}...\")\n    t = time()\n    \n    maps = pycolmap.incremental_mapping(\n        database_path=database_path,\n        image_path=images_dir,\n        output_path=output_path,\n    )\n    \n    params.laptime_reconstruction = time() - t\n    print(f'Reconstruction done in {params.laptime_reconstruction:.4f} sec')\n    print(f'Generated {len(maps)} 3D models')\n    \n    params.maps = maps\n    params.sparse_output_path = output_path\n\n    if maps and len(maps) > 0:\n        update_predictions_from_maps(params, maps)\n        print(f'✅ Extracted poses from {len(maps)} models')\n    else:\n        print(f'⚠️  No reconstruction models generated for {params.dataset}')\n    \n    return params\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def update_predictions_from_maps(params, maps):\n    \"\"\"Reflect reconstruction results in predictions\"\"\"\n    filename_to_index = params.filename_to_index\n    predictions = params.predictions\n    \n    if not isinstance(maps, dict):\n        return\n    \n    # Sort by model size\n    list_num_images = [len(rec.images) for rec in maps.values()]\n    list_num_images = np.array(list_num_images)\n    print(f\"Model sizes: {list_num_images}\")\n    \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    \n    sort_idx = np.argsort(list_num_images)\n    \n    # Extract image information from each model\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        \n        if cur_map_size < 3:\n            continue\n        \n        for index, image in cur_map.images.items():\n            if image.name not in filename_to_index:\n                continue\n            \n            prediction_index = filename_to_index[image.name]\n            \n            if cur_map_size > predictions[prediction_index].map_size:\n                predictions[prediction_index].map_size = cur_map_size\n                predictions[prediction_index].cluster_index = map_index\n                predictions[prediction_index].retry_index = 0\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    \n    print(f\"Registered {registered} images in reconstruction\")\n\n# =========================================================\n# Main Pipeline\n# =========================================================\ndef process_dataset(params):\n    \"\"\"Processing pipeline for the whole dataset\"\"\"\n    # Feature extraction and matching\n    params = exec_feature_extraction_and_matching(params)\n    \n    # 3D reconstruction\n    params = exec_reconstruction(params)\n    \n    return params","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# Main Execution (Updated: Only process existing datasets)\n# =========================================================\nimport os\nimport pandas as pd\nimport gc\nimport concurrent.futures\nfrom pathlib import Path\n\nis_train = False\ndata_dir = '/kaggle/input/image-matching-challenge-2025'\nworkdir = '/tmp/kaggle/working/result/'\n\nif is_train:\n    sample_submission_csv = os.path.join(data_dir, 'train_labels.csv')\n    base_dir = os.path.join(data_dir, 'train')\nelse:\n    sample_submission_csv = os.path.join(data_dir, 'sample_submission.csv')\n    base_dir = os.path.join(data_dir, 'test')\n\nsamples = {}\ndataset_logs = []\ncompetition_data = pd.read_csv(sample_submission_csv)\nos.makedirs(workdir, exist_ok=True)\n\n# Load data\nfor _, row in competition_data.iterrows():\n    if row.dataset not in samples:\n        samples[row.dataset] = []\n    prediction = Prediction(\n        image_id=None if is_train else row.image_id,\n        dataset=row.dataset,\n        filename=row.image\n    )\n    samples[row.dataset].append(prediction)\n\n# =========================================================\n# IMPORTANT: Filter only for datasets that actually exist\n# =========================================================\nprint(\"=\" * 70)\nprint(\"Checking dataset availability...\")\nprint(\"=\" * 70)\n\nexisting_datasets = {}\nfor dataset in samples:\n    dataset_path = os.path.join(base_dir, dataset)\n    \n    if os.path.exists(dataset_path) and os.path.isdir(dataset_path):\n        # Further verify if images exist\n        image_files = [f for f in os.listdir(dataset_path) \n                      if f.endswith(('.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG'))]\n        \n        if len(image_files) > 0:\n            existing_datasets[dataset] = samples[dataset]\n            print(f'✅ {dataset:30s} -> {len(samples[dataset])} images (found {len(image_files)} files)')\n        else:\n            print(f'⚠️  {dataset:30s} -> Directory exists but no images found (skipping)')\n    else:\n        print(f'❌ {dataset:30s} -> Directory does not exist (skipping)')\n\nprint(\"\\n\" + \"=\" * 70)\nprint(f\"To be processed: {len(existing_datasets)} / {len(samples)} datasets\")\nprint(\"=\" * 70)\n\n# Target only existing datasets\nsamples = existing_datasets\n\ngc.collect()\n\ndatasets_to_process = None  # Specify here if you want to process only specific datasets\n\n# =========================================================\n# Process each dataset in parallel\n# =========================================================\nwith concurrent.futures.ThreadPoolExecutor(max_workers=CONFIG.num_pallalel_sfm) as executor:\n    futures = {}\n    \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(base_dir, dataset)\n        images = [os.path.join(images_dir, p.filename) for p in predictions]\n        \n        # Filter only for images that actually exist\n        existing_images = [img for img in images if os.path.exists(img)]\n        \n        if len(existing_images) == 0:\n            print(f'⚠️  Dataset \"{dataset}\": No images found. Skipping.')\n            continue\n        \n        if len(existing_images) != len(images):\n            print(f'⚠️  Dataset \"{dataset}\": {len(existing_images)} of {len(images)} images exist')\n        \n        print(f'\\nProcessing: Dataset \"{dataset}\": {len(existing_images)} images')\n        \n        filename_to_index = {p.filename: idx for idx, p in enumerate(predictions)}\n        feature_dir = os.path.join(workdir, 'featureout', dataset)\n        os.makedirs(feature_dir, exist_ok=True)\n        \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=existing_images,  # Use only existing images\n        )\n        \n        # Add process to queue\n        futures[dataset] = executor.submit(process_dataset, dataset_params)\n    \n    # Collect results\n    for dataset in list(samples.keys()):\n        if datasets_to_process and dataset not in datasets_to_process:\n            continue\n        \n        if dataset not in futures:\n            print(f'⚠️  Dataset \"{dataset}\": Was not processed')\n            continue\n        \n        try:\n            dataset_params = futures[dataset].result()\n            samples[dataset] = dataset_params.predictions\n            dataset_logs.append(dataset_params)\n            print(f'✅ Dataset \"{dataset}\": Processing complete')\n        except Exception as e:\n            print(f'❌ Dataset \"{dataset}\": Error - {e}')\n            # Datasets with errors are kept in samples but will result in NaN in submission\n        \n        gc.collect()\n\n# =========================================================\n# Create 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'\n\nprint(\"\\n\" + \"=\" * 70)\nprint(\"Creating submission file...\")\nprint(\"=\" * 70)\n\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'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'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\nprint(f'✅ Submission file created: {submission_file}')\nprint(f'   Processed datasets: {len(samples)}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Display summary\nprint(\"\\n\" + \"=\"*100)\nfor dataset_params in dataset_logs:\n    dataset_params.summary()\n    print(\"=\"*100)\n\n# Calculate score if training data\nif 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'Computed metric in: {time() - t:.02f} sec.')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}