{"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":"none","dataSources":[{"sourceId":91498,"databundleVersionId":11655853,"sourceType":"competition"},{"sourceId":11924468,"sourceType":"datasetVersion","datasetId":6988459},{"sourceId":289691242,"sourceType":"kernelVersion"}],"dockerImageVersionId":30918,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"# **IMC2025 3D Reconstruction w/ vocab_tree_retriever**\n","metadata":{},"attachments":{}},{"cell_type":"markdown","source":"# COLMAP 3D Reconstruction Pipeline\n\n## Overview\nThis pipeline performs Structure-from-Motion (SfM) to estimate camera poses (rotation & translation) for each image using COLMAP.\n\n## Pipeline Steps\n\n### 1. **Feature Extraction** (CPU mode)\n- Extracts SIFT features from images\n- Creates `colmap.db` database\n- Settings: max 8192 features per image, 1024px max size\n\n### 2. **Vocab Tree Retriever** (optional)\n- Uses vocabulary tree to find similar image pairs\n- Outputs `image_pairs.txt`\n- Falls back to exhaustive matching if vocab tree unavailable\n\n### 3. **Matches Importer**\n- Imports pre-computed image pairs into database\n- Skipped if no vocab tree retriever output\n\n### 4. **Sequential Matcher** (CPU mode)\n- Matches features between sequential images\n- Optional loop detection using vocab tree\n- Overlap: 10 images, quadratic overlap enabled\n\n### 5. **Mapper (3D Reconstruction)**\n- Performs incremental SfM reconstruction\n- Outputs sparse 3D model to `sparse/0/`\n- Creates: `cameras.bin`, `images.bin`, `points3D.bin`\n\n### 6. **Pose Extraction** ⭐ **(Critical Fix)**\n- Converts model to TXT format\n- Reads camera poses from `images.txt`\n- Extracts rotation matrix (3×3) and translation vector (3×1)\n- Updates `Prediction` objects with poses\n\n### 7. **Submission Generation**\n- Writes poses to CSV: `rotation_matrix` (9 values), `translation_vector` (3 values)\n- Images without poses → NaN values\n\n","metadata":{}},{"cell_type":"code","source":"!python /kaggle/input/download-colmap/install_offline.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T06:57:57.615741Z","iopub.execute_input":"2026-01-02T06:57:57.616218Z","iopub.status.idle":"2026-01-02T06:57:57.995643Z","shell.execute_reply.started":"2026-01-02T06:57:57.616184Z","shell.execute_reply":"2026-01-02T06:57:57.994227Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!colmap help","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\nsys.path.append('/kaggle/input/imc25-utils')\nfrom database import *\nfrom h5_to_db import *\nimport metric\n\nimport concurrent.futures\n\nenv = os.environ.copy()\nenv['QT_QPA_PLATFORM'] = 'offscreen'  \nenv['DISPLAY'] = ''","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T17:04:32.531645Z","iopub.execute_input":"2026-01-01T17:04:32.531906Z","iopub.status.idle":"2026-01-01T17:04:32.54315Z","shell.execute_reply.started":"2026-01-01T17:04:32.531884Z","shell.execute_reply":"2026-01-01T17:04:32.541929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# Configuration\n# =========================================================\nclass CONFIG:\n    # vocab_tree_matcher settings\n    vocab_tree_path = '/kaggle/input/download-colmap/downloads/vocab_tree.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,"execution":{"iopub.status.busy":"2026-01-01T17:04:32.531645Z","iopub.execute_input":"2026-01-01T17:04:32.531906Z","iopub.status.idle":"2026-01-01T17:04:32.54315Z","shell.execute_reply.started":"2026-01-01T17:04:32.531884Z","shell.execute_reply":"2026-01-01T17:04:32.541929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================================\n# 5. COLMAP with Vocab Tree Retriever + Sequential Matcher (CORRECTED)\n# ====================================================================\ndef run_colmap_sequential_with_loop_detection(image_dir, workspace_dir, loop_detection=True, vocab_tree_path=None):\n    \"\"\"\n    Run COLMAP with vocab_tree_retriever + matches_importer + sequential_matcher\n\n    Workflow:\n    1. Feature extraction\n    2. vocab_tree_retriever - creates image pair list\n    3. matches_importer - imports the pair list\n    4. sequential_matcher - performs matching\n    5. mapper - reconstruction\n\n    Args:\n        image_dir: Directory containing input images\n        workspace_dir: COLMAP workspace directory\n        loop_detection: Enable loop detection for better reconstruction\n        vocab_tree_path: Path to vocabulary tree (required for retriever)\n    \"\"\"\n    print(\"\\n\" + \"=\" * 70)\n    print(\"Running COLMAP: vocab_tree_retriever → matches_importer → sequential_matcher\")\n    print(\"=\" * 70)\n\n    # Create workspace structure\n    os.makedirs(workspace_dir, exist_ok=True)\n    database_path = os.path.join(workspace_dir, \"database.db\")\n    sparse_dir = os.path.join(workspace_dir, \"sparse\")\n    image_pairs_path = os.path.join(workspace_dir, \"image_pairs.txt\")\n    os.makedirs(sparse_dir, exist_ok=True)\n\n    # Step 1: Feature Extraction\n    print(\"\\n[1/5] Extracting features...\")\n    try:\n        cmd = [\n            'colmap', 'feature_extractor',\n            '--database_path', database_path,\n            '--image_path', image_dir,\n            '--ImageReader.single_camera', '0',\n            '--ImageReader.camera_model', 'OPENCV',\n            '--SiftExtraction.max_image_size', '1024',\n            '--SiftExtraction.max_num_features', '8192'\n        ]\n        result = subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=3600,  env=env)\n        print(\"✓ Feature extraction complete\")\n    except subprocess.TimeoutExpired:\n        print(\"✗ Feature extraction timed out (>1 hour)\")\n        return False\n    except subprocess.CalledProcessError as e:\n        print(f\"✗ Feature extraction failed: {e.stderr}\")\n        return False\n    except Exception as e:\n        print(f\"✗ Feature extraction error: {e}\")\n        return False\n\n    # Step 2: Vocab Tree Retriever (creates image pair list)\n    print(\"\\n[2/5] Running vocab_tree_retriever to find similar image pairs...\")\n    if vocab_tree_path and os.path.exists(vocab_tree_path):\n        try:\n            cmd = [\n                'colmap', 'vocab_tree_retriever',\n                '--database_path', database_path,\n                '--vocab_tree_path', vocab_tree_path,\n                '--output_index', image_pairs_path,\n                '--num_images', '50',\n                #'--num_verifications', '10'\n            ]\n            result = subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=3600, env=env)\n\n            if os.path.exists(image_pairs_path):\n                print(f\"✓ Vocab tree retriever complete\")\n                print(f\"  Created image pairs list: {image_pairs_path}\")\n            else:\n                print(\"⚠ Image pairs file not created, continuing without retriever\")\n                image_pairs_path = None\n\n        except subprocess.TimeoutExpired:\n            print(\"⚠ Vocab tree retriever timed out - continuing without it\")\n            image_pairs_path = None\n        except subprocess.CalledProcessError as e:\n            print(f\"⚠ Vocab tree retriever failed: {e.stderr}\")\n            print(\"  Continuing without retriever...\")\n            image_pairs_path = None\n        except Exception as e:\n            print(f\"⚠ Vocab tree retriever error: {e}\")\n            print(\"  Continuing without retriever...\")\n            image_pairs_path = None\n    else:\n        print(\"⚠ Vocab tree not available - skipping retriever step\")\n        image_pairs_path = None\n\n    # Step 3: Matches Importer (imports the pair list if available)\n    if image_pairs_path and os.path.exists(image_pairs_path):\n        print(\"\\n[3/5] Importing image pair matches...\")\n        try:\n            cmd = [\n                'colmap', 'matches_importer',\n                '--database_path', database_path,\n                '--match_list_path', image_pairs_path,\n                '--match_type', 'pairs'\n            ]\n            result = subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=3600, env=env)\n            print(\"✓ Image pair matches imported\")\n        except subprocess.TimeoutExpired:\n            print(\"⚠ Matches importer timed out\")\n        except subprocess.CalledProcessError as e:\n            print(f\"⚠ Matches importer failed: {e.stderr}\")\n        except Exception as e:\n            print(f\"⚠ Matches importer error: {e}\")\n    else:\n        print(\"\\n[3/5] Skipping matches_importer (no image pairs list)\")\n\n    # Step 4: Sequential Matching (with optional loop detection)\n    print(\"\\n[4/5] Running sequential matching...\")\n    try:\n        cmd = [\n            'colmap', 'sequential_matcher',\n            '--database_path', database_path,\n            '--SequentialMatching.overlap', '10',\n            '--SequentialMatching.quadratic_overlap', '1'\n        ]\n\n        # Add loop detection if enabled and vocab tree is available\n        if loop_detection and vocab_tree_path and os.path.exists(vocab_tree_path):\n            print(\"  → Loop detection enabled with vocab tree\")\n            cmd.extend([\n                '--SequentialMatching.loop_detection', '1',\n                '--SequentialMatching.vocab_tree_path', vocab_tree_path,\n                '--SequentialMatching.loop_detection_num_images', '50'\n            ])\n        else:\n            if loop_detection:\n                print(\"  ⚠ Loop detection requested but vocab tree not available\")\n            print(\"  → Using sequential matching only\")\n            cmd.extend(['--SequentialMatching.loop_detection', '0'])\n\n        result = subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=3600,  env=env)\n        print(\"✓ Sequential matching complete\")\n    except subprocess.TimeoutExpired:\n        print(\"✗ Sequential matching timed out (>1 hour)\")\n        return False\n    except subprocess.CalledProcessError as e:\n        print(f\"✗ Sequential matching failed: {e.stderr}\")\n        return False\n    except Exception as e:\n        print(f\"✗ Sequential matching error: {e}\")\n        return False\n\n    # Step 5: Sparse Reconstruction (Mapper)\n    print(\"\\n[5/5] Running sparse reconstruction...\")\n    try:\n        cmd = [\n            'colmap', 'mapper',\n            '--database_path', database_path,\n            '--image_path', image_dir,\n            '--output_path', sparse_dir,\n            '--Mapper.ba_refine_focal_length', '1',\n            '--Mapper.ba_refine_principal_point', '1',\n            '--Mapper.ba_refine_extra_params', '1',\n            '--Mapper.min_num_matches', '15'\n        ]\n        result = subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=7200,  env=env)\n        print(\"✓ Sparse reconstruction complete\")\n\n        # Check if reconstruction was successful\n        model_dirs = [d for d in os.listdir(sparse_dir) if os.path.isdir(os.path.join(sparse_dir, d))]\n        if not model_dirs:\n            print(\"✗ No reconstruction models created\")\n            return False\n\n        print(f\"✓ Created {len(model_dirs)} reconstruction model(s)\")\n        # Find the largest model\n        largest_model = max(model_dirs, key=lambda d: len(os.listdir(os.path.join(sparse_dir, d))))\n        print(f\"  Main model: {largest_model}\")\n        largest_model_path = os.path.join(sparse_dir, largest_model)\n\n        # Convert to PLY format\n        print(\"\\nConverting to PLY format...\")\n        ply_output_path = os.path.join(sparse_dir, largest_model, \"points3D.ply\")\n\n        try:\n            cmd = [\n                'colmap', 'model_converter',\n                '--input_path', largest_model_path,\n                '--output_path', largest_model_path,\n                '--output_type', 'TXT'\n            ]\n            subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=600,  env=env)\n            print(\"✓ Converted to text format\")\n\n            # Convert points3D.txt to PLY\n            cmd = [\n                'colmap', 'model_converter',\n                '--input_path', largest_model_path,\n                '--output_path', largest_model_path,\n                '--output_type', 'PLY'\n            ]\n            subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=600,  env=env)\n\n            if os.path.exists(ply_output_path):\n                print(f\"✓ Point cloud PLY created: {ply_output_path}\")\n\n                # Also create a copy named point_cloud.ply for convenience\n                point_cloud_path = os.path.join(sparse_dir, largest_model, \"point_cloud.ply\")\n                if ply_output_path != point_cloud_path:\n                    shutil.copy(ply_output_path, point_cloud_path)\n                    print(f\"✓ Copied to: {point_cloud_path}\")\n\n                return True\n            else:\n                print(\"⚠ PLY file not found, but reconstruction completed\")\n                return True\n\n        except Exception as e:\n            print(f\"⚠ PLY conversion warning: {e}\")\n            print(\"  Binary model files are available, but PLY not created\")\n            return True\n\n    except subprocess.TimeoutExpired:\n        print(\"✗ Sparse reconstruction timed out (>2 hours)\")\n        return False\n    except subprocess.CalledProcessError as e:\n        print(f\"✗ Sparse reconstruction failed: {e.stderr}\")\n        return False\n    except Exception as e:\n        print(f\"✗ Sparse reconstruction error: {e}\")\n        return False\n\n\n# ====================================================================\n# 6. Gaussian Splatting Training\n# ====================================================================\ndef train_gaussian_splatting(source_path, model_path, iterations=3000):\n    \"\"\"\n    Train Gaussian Splatting model\n\n    Args:\n        source_path: Path to COLMAP workspace (contains sparse/ and images/)\n        model_path: Output path for trained model\n        iterations: Number of training iterations\n    \"\"\"\n    print(\"\\n\" + \"=\" * 70)\n    print(\"Training Gaussian Splatting\")\n    print(\"=\" * 70)\n\n    try:\n        cmd = [\n            sys.executable, 'train.py',\n            '-s', source_path,\n            '-m', model_path,\n            '--iterations', str(iterations),\n            '--test_iterations', '-1',  # Skip test evaluation\n            '--save_iterations', '-1'   # Only save final model\n        ]\n\n        print(f\"Training for {iterations} iterations...\")\n        print(\"This may take 30-60 minutes...\")\n\n        subprocess.run(cmd, cwd=WORK_DIR, check=True, timeout=7200,  env=env)\n        print(\"✓ Training complete\")\n        return True\n\n    except subprocess.TimeoutExpired:\n        print(\"✗ Training timed out (>2 hours)\")\n        return False\n    except subprocess.CalledProcessError as e:\n        print(f\"✗ Training failed: {e}\")\n        return False\n    except Exception as e:\n        print(f\"✗ Training error: {e}\")\n        return False\n\n\n# ====================================================================\n# 7. Video Rendering and GIF Creation\n# ====================================================================\ndef render_video(model_path, output_video_path, iteration=3000):\n    \"\"\"Generate video from the trained model by rendering a sequence of views\"\"\"\n    print(\"\\n\" + \"=\" * 70)\n    print(\"Rendering video from trained model\")\n    print(\"=\" * 70)\n\n    try:\n        # Execute rendering\n        cmd = [\n            sys.executable, 'render.py',\n            '-m', model_path,\n            '--iteration', str(iteration)\n        ]\n\n        print(\"Rendering images...\")\n        subprocess.run(cmd, cwd=WORK_DIR, check=True, timeout=1800,  env=env)\n\n        # Find the rendering directory\n        possible_dirs = [\n            f\"{model_path}/test/ours_{iteration}/renders\",\n            f\"{model_path}/train/ours_{iteration}/renders\",\n        ]\n\n        render_dir = None\n        for test_dir in possible_dirs:\n            if os.path.exists(test_dir):\n                render_dir = test_dir\n                print(f\"✓ Rendering directory found: {render_dir}\")\n                break\n\n        if not render_dir or not os.path.exists(render_dir):\n            print(\"✗ Rendering directory not found\")\n            return False\n\n        # Sort rendered PNG images for correct video sequence\n        render_imgs = sorted([f for f in os.listdir(render_dir) if f.endswith('.png')])\n\n        if not render_imgs:\n            print(\"✗ No rendered images found\")\n            return False\n\n        print(f\"✓ Found {len(render_imgs)} rendered images\")\n\n        # Create video with ffmpeg\n        print(\"Creating video...\")\n        subprocess.run([\n            'ffmpeg', '-y',\n            '-framerate', '30',\n            '-pattern_type', 'glob',\n            '-i', f\"{render_dir}/*.png\",\n            '-c:v', 'libx264',\n            '-pix_fmt', 'yuv420p',\n            '-crf', '18',\n            output_video_path\n        ], check=True, capture_output=True, timeout=600,  env=env)\n\n        if os.path.exists(output_video_path):\n            size_mb = os.path.getsize(output_video_path) / (1024 * 1024)\n            print(f\"✓ Video saved: {output_video_path} ({size_mb:.2f} MB)\")\n            return True\n        else:\n            print(\"✗ Video file not created\")\n            return False\n\n    except subprocess.TimeoutExpired:\n        print(\"✗ Rendering timed out\")\n        return False\n    except subprocess.CalledProcessError as e:\n        print(f\"✗ Rendering failed: {e}\")\n        return False\n    except Exception as e:\n        print(f\"✗ Rendering error: {e}\")\n        return False\n\n\ndef create_gif(video_path, gif_path, scale=720, fps=10, speed_factor=8):\n    \"\"\"\n    Create an animated GIF from an MP4 video file\n\n    Args:\n        video_path: Input MP4 video path\n        gif_path: Output GIF path\n        scale: Width in pixels (height auto-calculated)\n        fps: Output frame rate\n        speed_factor: Slow down factor (higher = slower)\n    \"\"\"\n    print(\"\\n\" + \"=\" * 70)\n    print(\"Creating animated GIF\")\n    print(\"=\" * 70)\n\n    if not os.path.exists(video_path):\n        print(f\"✗ Video file not found: {video_path}\")\n        return False\n\n    try:\n        print(f\"Converting video to GIF (scale={scale}px, fps={fps}, {speed_factor}x slower)...\")\n\n        subprocess.run([\n            'ffmpeg', '-y',\n            '-i', video_path,\n            '-vf', f'setpts={speed_factor}*PTS,fps={fps},scale={scale}:-1:flags=lanczos',\n            '-loop', '0',\n            gif_path\n        ], check=True, capture_output=True, timeout=600,  env=env)\n\n        if os.path.exists(gif_path):\n            size_mb = os.path.getsize(gif_path) / (1024 * 1024)\n            print(f\"✓ GIF creation complete: {gif_path} ({size_mb:.2f} MB)\")\n            return True\n        else:\n            print(\"✗ GIF file not created\")\n            return False\n\n    except subprocess.TimeoutExpired:\n        print(\"✗ GIF creation timed out\")\n        return False\n    except subprocess.CalledProcessError as e:\n        print(f\"✗ GIF creation failed: {e}\")\n        return False\n    except Exception as e:\n        print(f\"✗ GIF creation error: {e}\")\n        return False\n\n\n# ====================================================================\n# 8. Complete Pipeline\n# ====================================================================\ndef run_complete_pipeline(image_folder, max_frames=300, iterations=3000):\n    \"\"\"\n    Run the complete Gaussian Splatting pipeline from images to GIF\n\n    Args:\n        image_folder: Path to input images\n        max_frames: Maximum number of frames to process\n        iterations: Training iterations\n    \"\"\"\n    print(\"\\n\" + \"=\" * 70)\n    print(\"COMPLETE GAUSSIAN SPLATTING PIPELINE\")\n    print(\"=\" * 70)\n\n    # Setup paths\n    processed_dir = os.path.join(OUTPUT_DIR, 'processed_images')\n    colmap_workspace = COLMAP_DIR\n    model_output = os.path.join(OUTPUT_DIR, 'trained_model')\n    video_path = os.path.join(OUTPUT_DIR, 'output.mp4')\n    gif_path = os.path.join(OUTPUT_DIR, 'output.gif')\n\n    # Step 1: Process images\n    print(\"\\n### Step 1/6: Processing Images ###\")\n    frame_count = process_frames_from_folder(image_folder, processed_dir, max_frames)\n    if frame_count == 0:\n        print(\"✗ Pipeline failed: No frames processed\")\n        return False\n\n    # Step 2: Normalize image sizes\n    print(\"\\n### Step 2/6: Normalizing Image Sizes ###\")\n    normalize_image_sizes(processed_dir, processed_dir, target_size=1024)\n\n    # Step 3: Download vocab tree\n    print(\"\\n### Step 3/6: Downloading Vocabulary Tree ###\")\n    vocab_tree_path = download_vocab_tree()\n\n    # Step 4: Run COLMAP\n    print(\"\\n### Step 4/6: Running COLMAP Reconstruction ###\")\n    success = run_colmap_sequential_with_loop_detection(\n        processed_dir,\n        colmap_workspace,\n        loop_detection=True,\n        vocab_tree_path=vocab_tree_path\n    )\n    if not success:\n        print(\"✗ Pipeline failed: COLMAP reconstruction failed\")\n        return False\n\n    # Step 5: Train Gaussian Splatting\n    print(\"\\n### Step 5/6: Training Gaussian Splatting ###\")\n    success = train_gaussian_splatting(colmap_workspace, model_output, iterations)\n    if not success:\n        print(\"✗ Pipeline failed: Training failed\")\n        return False\n\n    # Step 6: Render video and create GIF\n    print(\"\\n### Step 6/6: Rendering Video and Creating GIF ###\")\n    if render_video(model_output, video_path, iteration=iterations):\n        create_gif(video_path, gif_path)\n\n    print(\"\\n\" + \"=\" * 70)\n    print(\"✓ PIPELINE COMPLETE!\")\n    print(\"=\" * 70)\n    print(f\"Output files:\")\n    print(f\"  - Video: {video_path}\")\n    print(f\"  - GIF: {gif_path}\")\n    print(f\"  - Model: {model_output}\")\n\n    return True","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import subprocess\nfrom pathlib import Path\n\ndef exec_feature_extraction_and_matching(params):\n    \"\"\"\n    COLMAP feature extraction and matching using the modern workflow.\n    Runs in CPU-only mode for headless environments.\n    \"\"\"\n    from pathlib import Path\n    import subprocess\n    import os\n    \n    db_path = Path(params.feature_dir) / \"colmap.db\"\n    img_dir = Path(params.images_dir)\n    image_pairs_path = Path(params.feature_dir) / \"image_pairs.txt\"\n    \n    # ================================================================\n    # CRITICAL: Set environment for headless execution\n    # ================================================================\n    env = os.environ.copy()\n    env['QT_QPA_PLATFORM'] = 'offscreen'\n    env['DISPLAY'] = ''\n    env['XDG_RUNTIME_DIR'] = '/tmp/runtime-root'  # Set runtime dir\n    \n    print(f\"\\n  [1/4] Feature Extraction (CPU mode)\")\n    print(f\"    Database: {db_path}\")\n    print(f\"    Images: {img_dir}\")\n    \n    # ================================================================\n    # Step 1: Feature Extraction (FORCE CPU MODE)\n    # ================================================================\n    try:\n        cmd = [\n            \"colmap\", \"feature_extractor\",\n            \"--database_path\", str(db_path),\n            \"--image_path\", str(img_dir),\n            \"--ImageReader.single_camera\", \"1\",\n            \"--ImageReader.camera_model\", \"OPENCV\",\n            \"--SiftExtraction.use_gpu\", \"0\",  # ← CRITICAL: Force CPU mode\n            \"--SiftExtraction.max_image_size\", \"1024\",\n            \"--SiftExtraction.max_num_features\", \"8192\"\n        ]\n        \n        result = subprocess.run(\n            cmd, \n            capture_output=True, \n            text=True, \n            timeout=1800, \n            check=False,\n            env=env\n        )\n        \n        if result.returncode != 0:\n            print(f\"    ✗ Feature extraction failed\")\n            print(f\"    Error: {result.stderr[:500]}\")\n            return params\n        \n        print(f\"    ✓ Feature extraction complete\")\n        \n    except Exception as e:\n        print(f\"    ✗ Error: {e}\")\n        return params\n    \n    # Check database\n    if not db_path.exists() or db_path.stat().st_size < 1000:\n        print(f\"    ✗ Database not created properly\")\n        return params\n    \n    print(f\"    Database size: {db_path.stat().st_size / 1024:.1f} KB\")\n    \n    # ================================================================\n    # Step 2: Vocab Tree Retriever (creates image pair list)\n    # ================================================================\n    vocab_tree_path = getattr(CONFIG, 'vocab_tree_path', None)\n    use_vocab_tree = vocab_tree_path and Path(vocab_tree_path).exists()\n    \n    if use_vocab_tree:\n        print(f\"\\n  [2/4] Vocab Tree Retriever\")\n        print(f\"    Vocab tree: {vocab_tree_path}\")\n        \n        try:\n            cmd = [\n                \"colmap\", \"vocab_tree_retriever\",\n                \"--database_path\", str(db_path),\n                \"--vocab_tree_path\", str(vocab_tree_path),\n                \"--output_index\", str(image_pairs_path),\n                \"--num_images\", \"100\"\n            ]\n            \n            result = subprocess.run(\n                cmd, \n                capture_output=True, \n                text=True, \n                timeout=1800, \n                check=False,\n                env=env\n            )\n            \n            if result.returncode != 0 or not image_pairs_path.exists():\n                print(f\"    ⚠ Retriever failed, will use exhaustive matching\")\n                if result.stderr:\n                    print(f\"    Error: {result.stderr[:300]}\")\n                use_vocab_tree = False\n            else:\n                print(f\"    ✓ Created image pairs: {image_pairs_path}\")\n                \n        except Exception as e:\n            print(f\"    ⚠ Retriever error: {e}, will use exhaustive matching\")\n            use_vocab_tree = False\n    else:\n        print(f\"\\n  [2/4] Vocab Tree Retriever - Skipped (no vocab tree)\")\n    \n    # ================================================================\n    # Step 3: Matches Importer (if using vocab tree retriever)\n    # ================================================================\n    if use_vocab_tree and image_pairs_path.exists():\n        print(f\"\\n  [3/4] Matches Importer\")\n        \n        try:\n            cmd = [\n                \"colmap\", \"matches_importer\",\n                \"--database_path\", str(db_path),\n                \"--match_list_path\", str(image_pairs_path),\n                \"--match_type\", \"pairs\"\n            ]\n            \n            result = subprocess.run(\n                cmd, \n                capture_output=True, \n                text=True, \n                timeout=1800, \n                check=False,\n                env=env\n            )\n            \n            if result.returncode != 0:\n                print(f\"    ⚠ Importer failed\")\n                if result.stderr:\n                    print(f\"    Error: {result.stderr[:300]}\")\n                use_vocab_tree = False\n            else:\n                print(f\"    ✓ Image pairs imported\")\n                \n        except Exception as e:\n            print(f\"    ⚠ Importer error: {e}\")\n            use_vocab_tree = False\n    else:\n        print(f\"\\n  [3/4] Matches Importer - Skipped\")\n    \n    # ================================================================\n    # Step 4: Sequential Matcher (with optional loop detection)\n    # ================================================================\n    print(f\"\\n  [4/4] Sequential Matcher (CPU mode)\")\n    \n    try:\n        cmd = [\n            \"colmap\", \"sequential_matcher\",\n            \"--database_path\", str(db_path),\n            \"--SiftMatching.use_gpu\", \"0\",  # ← CRITICAL: Force CPU mode\n            \"--SequentialMatching.overlap\", \"10\",\n            \"--SequentialMatching.quadratic_overlap\", \"1\"\n        ]\n        \n        # Add loop detection if vocab tree is available\n        if use_vocab_tree:\n            print(f\"    Mode: Sequential + Loop detection\")\n            cmd.extend([\n                \"--SequentialMatching.loop_detection\", \"1\",\n                \"--SequentialMatching.vocab_tree_path\", str(vocab_tree_path),\n                \"--SequentialMatching.loop_detection_num_images\", \"50\"\n            ])\n        else:\n            print(f\"    Mode: Sequential only\")\n            cmd.extend([\"--SequentialMatching.loop_detection\", \"0\"])\n        \n        result = subprocess.run(\n            cmd, \n            capture_output=True, \n            text=True, \n            timeout=3600, \n            check=False,\n            env=env\n        )\n        \n        if result.returncode != 0:\n            print(f\"    ✗ Sequential matcher failed\")\n            print(f\"    Error: {result.stderr[:500]}\")\n            \n            # Fallback to exhaustive matcher\n            print(f\"\\n    Trying exhaustive_matcher as fallback...\")\n            cmd = [\n                \"colmap\", \"exhaustive_matcher\",\n                \"--database_path\", str(db_path),\n                \"--SiftMatching.use_gpu\", \"0\",  # ← CPU mode\n                \"--SiftMatching.guided_matching\", \"1\"\n            ]\n            \n            result = subprocess.run(\n                cmd, \n                capture_output=True, \n                text=True, \n                timeout=3600, \n                check=False,\n                env=env\n            )\n            \n            if result.returncode != 0:\n                print(f\"    ✗ Exhaustive matcher also failed\")\n                print(f\"    Error: {result.stderr[:500]}\")\n                return params\n            else:\n                print(f\"    ✓ Exhaustive matcher complete (fallback)\")\n        else:\n            print(f\"    ✓ Sequential matcher complete\")\n        \n    except subprocess.TimeoutExpired:\n        print(f\"    ✗ Matching timed out\")\n        return params\n    except Exception as e:\n        print(f\"    ✗ Error: {e}\")\n        return params\n    \n    return params","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def incremental_mapping(database_path, image_path, output_path):\n    \"\"\"\n    Run COLMAP mapper (reconstruction)\n    \"\"\"\n    import subprocess\n    import os\n    from pathlib import Path\n    \n    output_path = Path(output_path)\n    output_path.mkdir(parents=True, exist_ok=True)\n    \n    # Set headless environment\n    env = os.environ.copy()\n    env['QT_QPA_PLATFORM'] = 'offscreen'\n    env['DISPLAY'] = ''\n    \n    print(f\"\\n  [5/5] Mapper (3D Reconstruction)\")\n    print(f\"    Database: {database_path}\")\n    print(f\"    Images: {image_path}\")\n    print(f\"    Output: {output_path}\")\n    \n    try:\n        cmd = [\n            'colmap', 'mapper',\n            '--database_path', str(database_path),\n            '--image_path', str(image_path),\n            '--output_path', str(output_path),\n            '--Mapper.ba_refine_focal_length', '1',\n            '--Mapper.ba_refine_principal_point', '1',\n            '--Mapper.ba_refine_extra_params', '1',\n            '--Mapper.min_num_matches', '15'\n        ]\n        \n        result = subprocess.run(\n            cmd, \n            capture_output=True, \n            text=True, \n            timeout=3600,\n            check=False,\n            env=env  # ← Use modified environment\n        )\n        \n        if result.returncode != 0:\n            print(f\"    ✗ Mapper failed (code {result.returncode})\")\n            print(f\"    Error: {result.stderr[:500]}\")\n        \n        # Find created model directories\n        model_dirs = []\n        if output_path.exists():\n            model_dirs = sorted([\n                d for d in output_path.iterdir() \n                if d.is_dir() and d.name.isdigit()\n            ])\n        \n        if model_dirs:\n            print(f\"    ✓ Created {len(model_dirs)} model(s): {[d.name for d in model_dirs]}\")\n        else:\n            print(f\"    ✗ No models created\")\n        \n        return model_dirs\n        \n    except subprocess.TimeoutExpired:\n        print(f\"    ✗ Mapper timed out\")\n        return []\n    except Exception as e:\n        print(f\"    ✗ Error: {e}\")\n        return []","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def read_model_poses(model_dir):\n    \"\"\"\n    Read camera poses from COLMAP model\n    \n    Returns:\n        dict: {image_name: {'rotation': R, 'translation': t}}\n    \"\"\"\n    from pathlib import Path\n    import numpy as np\n    \n    images_txt = Path(model_dir) / \"images.txt\"\n    \n    if not images_txt.exists():\n        print(f\"  Warning: images.txt not found in {model_dir}\")\n        return {}\n    \n    poses = {}\n    \n    with open(images_txt, 'r') as f:\n        lines = f.readlines()\n    \n    # Parse images.txt format:\n    # IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME\n    for i in range(len(lines)):\n        line = lines[i].strip()\n        \n        # Skip comments and empty lines\n        if not line or line.startswith('#'):\n            continue\n        \n        parts = line.split()\n        if len(parts) < 10:\n            continue\n        \n        try:\n            # Extract pose data\n            qw, qx, qy, qz = map(float, parts[1:5])\n            tx, ty, tz = map(float, parts[5:8])\n            image_name = parts[9]  # filename\n            \n            # Convert quaternion to rotation matrix\n            # Quaternion to rotation matrix formula\n            R = np.array([\n                [1 - 2*(qy**2 + qz**2), 2*(qx*qy - qw*qz), 2*(qx*qz + qw*qy)],\n                [2*(qx*qy + qw*qz), 1 - 2*(qx**2 + qz**2), 2*(qy*qz - qw*qx)],\n                [2*(qx*qz - qw*qy), 2*(qy*qz + qw*qx), 1 - 2*(qx**2 + qy**2)]\n            ])\n            \n            t = np.array([tx, ty, tz])\n            \n            poses[image_name] = {\n                'rotation': R,\n                'translation': t\n            }\n            \n        except (ValueError, IndexError) as e:\n            continue\n    \n    print(f\"  Read poses for {len(poses)} images from {model_dir.name}\")\n    return poses","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    from time import time\n    from pathlib import Path\n    import subprocess\n    import os\n    \n    # Set environment for headless execution\n    env = os.environ.copy()\n    env['QT_QPA_PLATFORM'] = 'offscreen'\n    env['DISPLAY'] = ''\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    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    # ========================================================\n    # CRITICAL FIX: Read poses from COLMAP reconstruction\n    # ========================================================\n    \n    if maps and len(maps) > 0:\n        # Use the largest/first model\n        largest_model = maps[0]\n        print(f\"\\nProcessing reconstruction model: {largest_model.name}\")\n        \n        # Convert model to TXT format (required for reading poses)\n        print(f\"Converting model to TXT format...\")\n        try:\n            cmd = [\n                'colmap', 'model_converter',\n                '--input_path', str(largest_model),\n                '--output_path', str(largest_model),\n                '--output_type', 'TXT'\n            ]\n            result = subprocess.run(\n                cmd, \n                check=True, \n                capture_output=True, \n                text=True,\n                timeout=300,\n                env=env\n            )\n            print(f\"  ✓ Model converted to TXT format\")\n        except subprocess.TimeoutExpired:\n            print(f\"  ⚠️ Model conversion timed out\")\n        except subprocess.CalledProcessError as e:\n            print(f\"  ⚠️ Model conversion failed: {e.stderr[:300]}\")\n        except Exception as e:\n            print(f\"  ⚠️ Model conversion error: {e}\")\n        \n        # Read poses from the model\n        print(f\"Reading camera poses from model...\")\n        poses = read_model_poses(largest_model)\n        \n        if poses:\n            print(f\"  ✓ Read {len(poses)} poses from reconstruction\")\n            \n            # Update Prediction objects with poses\n            updated_count = 0\n            missing_count = 0\n            \n            for prediction in params.predictions:\n                if prediction.filename in poses:\n                    pose = poses[prediction.filename]\n                    prediction.rotation = pose['rotation']\n                    prediction.translation = pose['translation']\n                    updated_count += 1\n                else:\n                    missing_count += 1\n                    # Keep rotation and translation as None\n                    # These will become NaN in submission\n            \n            print(f\"  ✓ Updated poses: {updated_count}/{len(params.predictions)} images\")\n            if missing_count > 0:\n                print(f\"  ⚠️ Missing poses: {missing_count} images (will be NaN in submission)\")\n        else:\n            print(f\"  ⚠️ No poses read from model - all poses will be NaN\")\n    else:\n        print(f\"⚠️ No reconstruction models created - all poses will be NaN\")\n    \n    # Save reconstruction results to params\n    params.maps = maps\n    params.sparse_output_path = output_path\n    \n    print(f\"✅ Dataset \\\"{params.dataset}\\\": Processing complete\\n\")\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 = '/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,"execution":{"iopub.status.busy":"2026-01-01T17:04:32.577216Z","iopub.execute_input":"2026-01-01T17:04:32.577539Z","iopub.status.idle":"2026-01-01T17:04:33.187671Z","shell.execute_reply.started":"2026-01-01T17:04:32.577499Z","shell.execute_reply":"2026-01-01T17:04:33.186653Z"}},"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}]}