{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":71885,"databundleVersionId":8143495,"sourceType":"competition"},{"sourceId":7884485,"sourceType":"datasetVersion","datasetId":4628051},{"sourceId":7884725,"sourceType":"datasetVersion","datasetId":4628331},{"sourceId":8539410,"sourceType":"datasetVersion","datasetId":5101247},{"sourceId":8560092,"sourceType":"datasetVersion","datasetId":5075608}],"dockerImageVersionId":30699,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"7ded0ec0-72bf-4612-8656-95a3b2cbc866","_cell_guid":"739264d0-bc82-4baa-9a45-447800747dae","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T03:32:11.024414Z","iopub.execute_input":"2025-09-16T03:32:11.025137Z","iopub.status.idle":"2025-09-16T03:32:12.18098Z","shell.execute_reply.started":"2025-09-16T03:32:11.025109Z","shell.execute_reply":"2025-09-16T03:32:12.18014Z"},"scrolled":true,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kagglehub\n\n# Download latest version\npath = kagglehub.dataset_download(\"soyaoki/sfd2-semantic-guidedfeaturedetectionanddescription\")\n\nprint(\"Path to dataset files:\", path)","metadata":{"_uuid":"2dc295b6-4524-4bf0-b6b8-5a629da4b512","_cell_guid":"f4faeed9-6e70-4b02-8914-95e0a2b1a592","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T03:32:12.182831Z","iopub.execute_input":"2025-09-16T03:32:12.183447Z","iopub.status.idle":"2025-09-16T03:32:12.206807Z","shell.execute_reply.started":"2025-09-16T03:32:12.183414Z","shell.execute_reply":"2025-09-16T03:32:12.205025Z"},"scrolled":true,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kagglehub\n\n# Download latest version\npath = kagglehub.dataset_download(\"oldufo/colmap-db-import\")\n\nprint(\"Path to dataset files:\", path)","metadata":{"_uuid":"419aeca7-bd19-45dc-9686-0c5a4e9d9ae3","_cell_guid":"a08db963-e274-4212-953c-17b6013fb003","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T03:32:12.207659Z","iopub.status.idle":"2025-09-16T03:32:12.207968Z","shell.execute_reply.started":"2025-09-16T03:32:12.207831Z","shell.execute_reply":"2025-09-16T03:32:12.207844Z"},"scrolled":true,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kagglehub\n\n# Download latest version\npath = kagglehub.dataset_download(\"soyaoki/hierarchical-localization\")\n\nprint(\"Path to dataset files:\", path)","metadata":{"_uuid":"87213090-653f-4636-86b8-49a9b229491e","_cell_guid":"5ba758bf-79a9-4f75-8bd8-a2e684311a83","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T03:32:12.208935Z","iopub.status.idle":"2025-09-16T03:32:12.209211Z","shell.execute_reply.started":"2025-09-16T03:32:12.209067Z","shell.execute_reply":"2025-09-16T03:32:12.209078Z"},"scrolled":true,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kagglehub\n\n# Download latest version\npath = kagglehub.dataset_download(\"oldufo/imc2024-packages-lightglue-rerun-kornia\")\n\nprint(\"Path to dataset files:\", path)","metadata":{"_uuid":"4fac7470-3358-4c5f-9467-cea97ed8c771","_cell_guid":"f392ad1e-bf5d-4bc5-a8e9-a2d628878a11","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T03:32:12.211434Z","iopub.status.idle":"2025-09-16T03:32:12.212124Z","shell.execute_reply.started":"2025-09-16T03:32:12.211879Z","shell.execute_reply":"2025-09-16T03:32:12.2119Z"},"scrolled":true,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nprint(sys.version)\nimport logging\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\nlogger = logging.getLogger(__name__)","metadata":{"_uuid":"19b81dc4-1644-405b-981d-7fa1e3ddbeed","_cell_guid":"0acbd69b-e42c-4e5b-ae58-99821408c0a6","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:24:54.093783Z","iopub.execute_input":"2025-09-16T04:24:54.094591Z","iopub.status.idle":"2025-09-16T04:24:54.0994Z","shell.execute_reply.started":"2025-09-16T04:24:54.09456Z","shell.execute_reply":"2025-09-16T04:24:54.098533Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install --no-index /kaggle/input/imc2024-packages-lightglue-rerun-kornia/* --no-deps","metadata":{"_uuid":"b752b03b-396f-4e6b-b83f-8bf457550adc","_cell_guid":"5b132a24-1b26-4f0f-844a-02070aee15fe","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:24:54.518251Z","iopub.execute_input":"2025-09-16T04:24:54.519041Z","iopub.status.idle":"2025-09-16T04:24:56.662924Z","shell.execute_reply.started":"2025-09-16T04:24:54.519011Z","shell.execute_reply":"2025-09-16T04:24:56.661706Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ⚙️ Environment Setup & Dependencies\n\nWe start by preparing the environment:\n\n1. Import core Python libraries (`sys`, `gc`, `numpy`, `tqdm`, etc.).\n2. Create a **Torch cache** directory (needed for pretrained models).\n3. Copy pretrained weights & third-party dependencies from Kaggle inputs.\n4. Append paths for **Hierarchical Localization (HLOC)** and **SFD2** feature extractor.\n5. Import the main modules: \n   - `hloc` (for feature extraction, matching, and reconstruction)\n   - `pycolmap` (for structure-from-motion back-end)\n   - `viz_3d` (for visualization utilities)\n","metadata":{}},{"cell_type":"code","source":"import sys\nimport gc\nimport numpy as np\nimport tqdm\nfrom pathlib import Path\nfrom copy import deepcopy\nfrom typing import Dict, List, Optional, Tuple, Any\nimport logging\n!mkdir -p /root/.cache/torch\n!cp -r /kaggle/input/hierarchical-localization/hub /root/.cache/torch\n!cp -r /kaggle/input/sfd2-semantic-guidedfeaturedetectionanddescription/weights ./\n\n\nimport sys\nsys.path.append(\"/kaggle/input/hierarchical-localization\")\nsys.path.append(\"/kaggle/input/hierarchical-localization/third_party\")\nsys.path.append(\"/kaggle/input/sfd2-semantic-guidedfeaturedetectionanddescription\")\n\ntry:\n    from hloc import (\n        pairs_from_exhaustive, \n        pairs_from_retrieval, \n        extract_features, \n        match_features, \n        match_dense, \n        reconstruction, \n        visualization,\n    )\n    from hloc.utils import viz_3d\n    import pycolmap\nexcept ImportError as e:\n    print(f\"Failed to import hloc or pycolmap: {e}\")","metadata":{"_uuid":"cf649d65-ab4e-4495-91f7-a242718123d8","_cell_guid":"43ed689c-2267-477b-bd73-edebc4d88024","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:24:56.665064Z","iopub.execute_input":"2025-09-16T04:24:56.665357Z","iopub.status.idle":"2025-09-16T04:25:11.112648Z","shell.execute_reply.started":"2025-09-16T04:24:56.66533Z","shell.execute_reply":"2025-09-16T04:25:11.111337Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🛠 SfM Pipeline Configuration Class\n\nThis cell defines the **`SfMConfiguration`** class, which manages all parameters for the Structure-from-Motion (SfM) pipeline.\n\n### Key Features:\n\n1. **Available Configurations**  \n   - **Retrieval**: `[\"dir\", \"netvlad\", \"openibl\", \"eigenplaces\"]`  \n   - **Feature Extractors**: `\"superpoint_aachen\", \"superpoint_max\", \"superpoint_inloc\", \"r2d2\", \"d2net-ss\", \"sift\", \"sosnet\", \"disk\", \"aliked-n16-rot\", \"aliked-n32\"`  \n   - **Feature Matchers**: `\"superpoint+lightglue\", \"disk+lightglue\", \"superglue\", \"superglue-fast\", \"NN-superpoint\", \"NN-ratio\", \"NN-mutual\", \"adalam\", \"aliked+lightglue\"`  \n   - **Dense Matchers**: `\"loftr\", \"loftr_aachen\", \"loftr_superpoint\",\"superpoint+lightglue\"`  \n\n2. **Default Configuration**  \n   - Retrieval: `\"netvlad\"`  \n   - Feature extractor: `\"aliked-n16-rot\"`  \n   - Dense matcher: `\"superpoint+lightglue\"`  \n   - Feature matcher: `\"aliked+lightglue\"`  \n   - Match method: `\"dense\"`  \n   - Maximum allowed reprojection error: `2 px`  \n   - Cell size for matching: `6`  \n   - Other parameters control number of matched pairs, exhaustive pairing, etc.\n\n3. **Methods**  \n   - `update_config(**kwargs)`: Update one or more configuration parameters.  \n   - `validate_config()`: Check if the current configuration is valid.  \n   - `print_config()`: Log the current configuration to console.  \n\nThis class ensures **flexibility and safety** when switching between different SfM settings and models.\n","metadata":{}},{"cell_type":"code","source":"class SfMConfiguration:\n    \"\"\"Configuration class for SfM pipeline parameters\"\"\"\n    \n    # Available configurations\n    RETRIEVAL_CONFIGS = [\"dir\", \"netvlad\", \"openibl\", \"eigenplaces\"]\n    FEATURE_CONFIGS = [\n        \"superpoint_aachen\", \"superpoint_max\", \"superpoint_inloc\", \n        \"r2d2\", \"d2net-ss\", \"sift\", \"sosnet\", \"disk\", \n        \"aliked-n16-rot\", \"aliked-n32\"\n    ]\n    MATCHER_FEATURE_CONFIGS = [\n        \"superpoint+lightglue\", \"disk+lightglue\", \"superglue\", \n        \"superglue-fast\", \"NN-superpoint\", \"NN-ratio\", \n        \"NN-mutual\", \"adalam\", \"aliked+lightglue\"\n    ]\n    MATCHER_DENSE_CONFIGS = [\"loftr\", \"loftr_aachen\", \"loftr_superpoint\",\"superpoint+lightglue\"]\n    \n    def __init__(self):\n        self.config = {\n            \"retrieval_conf\": \"netvlad\",\n            \"feature_conf\": \"aliked-n16-rot\",\n            \"feature_conf_sfd2\": \"ressegnetv2-20220810-wapv2-sd2mfsf-uspg-0001-n4096-r1024\",\n            \"match_method\": \"dense\",  # \"feature\" or \"dense\"\n            \"matcher_conf_feature\": \"aliked+lightglue\",\n            \"matcher_conf_dense\": \"superpoint+lightglue\",\n            \"max_error\": 2,\n            \"cell_size\": 6,\n            \"num_matched_pairs\": 10,\n            \"use_exhaustive_pairs\": False,\n            \"max_images_exhaustive\": 50,\n        }\n    \n    def update_config(self, **kwargs):\n        \"\"\"Update configuration parameters\"\"\"\n        for key, value in kwargs.items():\n            if key in self.config:\n                self.config[key] = value\n                logger.info(f\"Updated {key}: {value}\")\n            else:\n                logger.warning(f\"Unknown configuration key: {key}\")\n    \n    def validate_config(self) -> bool:\n        \"\"\"Validate current configuration\"\"\"\n        if self.config[\"retrieval_conf\"] not in self.RETRIEVAL_CONFIGS:\n            logger.error(f\"Invalid retrieval config: {self.config['retrieval_conf']}\")\n            return False\n        \n        if self.config[\"feature_conf\"] not in self.FEATURE_CONFIGS:\n            logger.error(f\"Invalid feature config: {self.config['feature_conf']}\")\n            return False\n        \n        if self.config[\"match_method\"] not in [\"feature\", \"dense\"]:\n            logger.error(f\"Invalid match method: {self.config['match_method']}\")\n            return False\n        \n        if self.config[\"match_method\"] == \"feature\":\n            if self.config[\"matcher_conf_feature\"] not in self.MATCHER_FEATURE_CONFIGS:\n                logger.error(f\"Invalid feature matcher: {self.config['matcher_conf_feature']}\")\n                return False\n        \n        if self.config[\"match_method\"] == \"dense\":\n            if self.config[\"matcher_conf_dense\"] not in self.MATCHER_DENSE_CONFIGS:\n                logger.error(f\"Invalid dense matcher: {self.config['matcher_conf_dense']}\")\n                return False\n        \n        return True\n    \n    def print_config(self):\n        \"\"\"Print current configuration\"\"\"\n        logger.info(\"Current SfM Configuration:\")\n        for key, value in self.config.items():\n            logger.info(f\"  {key}: {value}\")","metadata":{"_uuid":"df5eabe3-f343-4b02-8b1e-3cebc8efc9c1","_cell_guid":"d202b077-6976-43ce-86fc-3577311a86fe","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:25:11.114313Z","iopub.execute_input":"2025-09-16T04:25:11.1146Z","iopub.status.idle":"2025-09-16T04:25:11.125687Z","shell.execute_reply.started":"2025-09-16T04:25:11.114576Z","shell.execute_reply":"2025-09-16T04:25:11.124816Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔗 Image Pair Generation Class\n\nThis cell defines the **`ImagePairGenerator`** class, which is responsible for generating **image pairs** for feature matching in the SfM pipeline.\n\n### Key Features:\n\n1. **Exhaustive Pair Generation**\n   - Method: `generate_exhaustive_pairs(image_paths, output_file)`  \n   - Generates all possible pairs between images.\n   - Best for **small image sets**.\n   - Steps:\n     - Logs the number of images.\n     - Creates the output directory if it does not exist.\n     - Converts absolute paths to **relative paths** for HLOC compatibility.\n     - Calls `pairs_from_exhaustive.main()` to generate the pairs.\n     - Logs the path of the generated pairs file.\n\n2. **Retrieval-Based Pair Generation**\n   - Method: `generate_retrieval_pairs(images_dir, output_dir, retrieval_conf, num_matched=10)`  \n   - Generates pairs based on **global image retrieval**, matching each image to its top-N similar images.\n   - Steps:\n     - Logs the retrieval configuration being used.\n     - Extracts global features for all images (`extract_features.main()`).\n     - Generates top-N retrieval-based pairs using `pairs_from_retrieval.main()`.\n     - Logs the path of the generated pairs file.\n\nThis class modularizes the **pair generation step**, allowing the SfM pipeline to switch easily between **exhaustive** and **retrieval-based** pairing strategies.\n","metadata":{}},{"cell_type":"code","source":"class ImagePairGenerator:\n    \"\"\"Generate image pairs for matching\"\"\"\n    \n    @staticmethod\n    def generate_exhaustive_pairs(image_paths: List[Path], output_file: Path) -> Path:\n        \"\"\"Generate exhaustive pairs for small image sets\"\"\"\n        logger.info(f\"Generating exhaustive pairs for {len(image_paths)} images\")\n        \n        output_file.parent.mkdir(parents=True, exist_ok=True)\n        \n        # Convert to relative paths for HLOC compatibility\n        relative_paths = [p.name for p in image_paths]\n        \n        pairs_from_exhaustive.main(\n            output_file, \n            image_list=relative_paths\n        )\n        \n        logger.info(f\"Generated exhaustive pairs: {output_file}\")\n        return output_file\n    \n    @staticmethod\n    def generate_retrieval_pairs(\n        images_dir: Path, \n        output_dir: Path, \n        retrieval_conf: str,\n        num_matched: int = 10\n    ) -> Path:\n        \"\"\"Generate pairs using retrieval-based matching\"\"\"\n        logger.info(f\"Generating retrieval pairs using {retrieval_conf}\")\n        \n        # Extract global features for retrieval\n        retrieval_config = extract_features.confs[retrieval_conf]\n        retrieval_path = extract_features.main(retrieval_config, images_dir, output_dir)\n        \n        # Generate pairs\n        pairs_file = output_dir / f\"pairs-{retrieval_conf}.txt\"\n        pairs_from_retrieval.main(retrieval_path, pairs_file, num_matched=num_matched)\n        \n        logger.info(f\"Generated {num_matched} retrieval pairs per image: {pairs_file}\")\n        return pairs_file","metadata":{"_uuid":"9ebf2e2f-031a-41d3-b57c-55aa813ff591","_cell_guid":"1076e8ea-e2ad-4813-af04-7136bb6ad99e","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:25:11.127645Z","iopub.execute_input":"2025-09-16T04:25:11.127904Z","iopub.status.idle":"2025-09-16T04:25:11.441853Z","shell.execute_reply.started":"2025-09-16T04:25:11.127883Z","shell.execute_reply":"2025-09-16T04:25:11.440929Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🖼️ Feature Extraction Class\n\nThis cell defines the **`FeatureExtractor`** class, which handles **extracting local features** from images for use in the SfM pipeline.\n\n### Key Points:\n\n- **Method:** `extract_features(images_dir, output_dir, feature_conf)`  \n- **Purpose:** Extracts local features (e.g., keypoints and descriptors) from all images in the directory.\n- **Inputs:**\n  - `images_dir`: Path to the folder containing images.\n  - `output_dir`: Path where extracted features will be saved.\n  - `feature_conf`: Name of the feature configuration to use (e.g., `superpoint`, `aliked-n16-rot`).\n- **Process:**\n  1. Logs the feature configuration being used.\n  2. Loads the configuration from `extract_features.confs`.\n  3. Calls `extract_features.main()` from HLOC to extract features.\n  4. Logs the path to the saved features.\n- **Returns:** Path to the saved feature file/directory.\n\nThis class modularizes **feature extraction**, allowing different detectors/descriptors to be used interchangeably in the pipeline.\n","metadata":{}},{"cell_type":"code","source":"class FeatureExtractor:\n    \"\"\"Extract features from images\"\"\"\n    \n    @staticmethod\n    def extract_features(\n        images_dir: Path,\n        output_dir: Path,\n        feature_conf: str\n    ) -> Path:\n        \"\"\"Extract features using specified configuration\"\"\"\n        logger.info(f\"Extracting features using {feature_conf}\")\n        \n        feature_config = extract_features.confs[feature_conf]\n        feature_path = extract_features.main(feature_config, images_dir, output_dir)\n        \n        logger.info(f\"Features extracted: {feature_path}\")\n        return feature_path","metadata":{"_uuid":"d588bd03-47b9-4103-9e08-2eee4a433c56","_cell_guid":"052d015e-2a72-46e7-b50a-35a24913ca9c","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:25:11.443235Z","iopub.execute_input":"2025-09-16T04:25:11.443566Z","iopub.status.idle":"2025-09-16T04:25:11.457626Z","shell.execute_reply.started":"2025-09-16T04:25:11.443538Z","shell.execute_reply":"2025-09-16T04:25:11.456635Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## FeatureMatcher Class\n\nHandles **matching image features** for SfM.\n\n- `match_features(...)`: Matches features using a sparse matcher (e.g., SuperGlue, ALIKED+LightGlue).  \n- `match_dense(...)`: Matches features densely (e.g., LoFTR or dense SuperPoint+LightGlue).  \n\n**Returns:** Paths to the matched features.\n","metadata":{}},{"cell_type":"code","source":"class FeatureMatcher:\n    \"\"\"Match features between images\"\"\"\n    \n    @staticmethod\n    def match_features(\n        pairs_file: Path,\n        feature_path: Path,\n        output_dir: Path,\n        matcher_conf: str,\n        max_error: int = 2,\n        cell_size: int = 6\n    ) -> Path:\n        \"\"\"Match features between image pairs\"\"\"\n        logger.info(f\"Matching features using {matcher_conf}\")\n        \n        matcher_config = match_features.confs[matcher_conf]\n        matcher_config = deepcopy(match_features.confs[matcher_conf])\n        matcher_config['max_error'] = max_error\n        matcher_config['cell_size'] = cell_size\n        match_path = output_dir / \"matches.h5\"\n        match_features.main(\n            matcher_config, \n            pairs=pairs_file,\n            features=feature_path,\n            matches=match_path   # <-- important fix\n        )\n        logger.info(f\"Features matched: {match_path}\")\n        return match_path\n    \n    @staticmethod\n    def match_dense(\n        pairs_file: Path,\n        images_dir: Path,\n        output_dir: Path,\n        matcher_conf: str\n    ) -> Tuple[Path, Path]:\n        \"\"\"Perform dense matching between image pairs\"\"\"\n        logger.info(f\"Dense matching using {matcher_conf}\")\n        \n        matcher_config = match_dense.confs[matcher_conf]\n        feature_path, match_path = match_dense.main(\n            matcher_config, \n            pairs_file, \n            images_dir, \n            export_dir=output_dir\n        )\n        \n        logger.info(f\"Dense matching completed: features={feature_path}, matches={match_path}\")\n        return feature_path, match_path","metadata":{"_uuid":"6a645b1c-79f1-4d6a-b3d9-0c71c119037d","_cell_guid":"1b069b7b-88ec-44cf-9d64-08cb2482526a","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:38:54.271254Z","iopub.execute_input":"2025-09-16T04:38:54.271629Z","iopub.status.idle":"2025-09-16T04:38:54.279794Z","shell.execute_reply.started":"2025-09-16T04:38:54.271603Z","shell.execute_reply":"2025-09-16T04:38:54.278993Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## SfMReconstructor Class\n\nPerforms **3D reconstruction** from matched image features.\n\n- `reconstruct(...)`: Runs SfM, generates a 3D model, and saves it.  \n- **Returns:** `pycolmap.Reconstruction` object containing camera poses and 3D points.\n","metadata":{}},{"cell_type":"code","source":"class SfMReconstructor:\n    \"\"\"3D reconstruction from matched features\"\"\"\n    \n    @staticmethod\n    def reconstruct(\n        images_dir: Path,\n        pairs_file: Path,\n        feature_path: Path,\n        match_path: Path,\n        output_dir: Path\n    ) -> pycolmap.Reconstruction:\n        \"\"\"Perform 3D reconstruction\"\"\"\n        logger.info(\"Starting 3D reconstruction\")\n        \n        sfm_dir = output_dir / \"sfm\"\n        sfm_dir.mkdir(parents=True, exist_ok=True)\n        \n        model = reconstruction.main(\n            sfm_dir,\n            images_dir,\n            pairs_file,\n            feature_path,\n            match_path\n        )\n        \n        # Save reconstruction\n        model.write_text(output_dir)\n        \n        logger.info(f\"3D reconstruction completed with {len(model.images)} images and {len(model.points3D)} 3D points\")\n        return model","metadata":{"_uuid":"6ca805df-7b9a-40c3-ae4c-56bcab5b1911","_cell_guid":"1c0c29fd-7194-41ec-9097-1afc428da83e","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:25:11.474727Z","iopub.execute_input":"2025-09-16T04:25:11.474978Z","iopub.status.idle":"2025-09-16T04:25:11.501452Z","shell.execute_reply.started":"2025-09-16T04:25:11.474958Z","shell.execute_reply":"2025-09-16T04:25:11.500659Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## SfMPipeline Class\n\nComplete **Structure-from-Motion (SfM) pipeline**:\n\n- `run(images_dir, output_dir)`: Executes SfM (pair generation → feature extraction/matching → 3D reconstruction).  \n- `_generate_pairs(...)`: Creates exhaustive or retrieval-based image pairs.  \n- `_extract_and_match_features(...)`: Extracts features and matches them (dense or feature-based).  \n- `visualize_3d(...)`: Interactive 3D visualization of the reconstruction.  \n- `visualize_2d(...)`: 2D projections of the model on images.  \n- `get_camera_poses()`: Returns camera rotation and translation matrices.  \n- `export_poses(output_file, format)`: Saves camera poses to TXT or CSV.\n","metadata":{}},{"cell_type":"code","source":"class SfMPipeline:\n    \"\"\"Complete Structure from Motion pipeline\"\"\"\n    \n    def __init__(self, config: Optional[SfMConfiguration] = None):\n        self.config = config or SfMConfiguration()\n        self.model = None\n        \n    def run(\n        self, \n        images_dir: Path, \n        output_dir: Path,\n        image_paths: Optional[List[Path]] = None\n    ) -> pycolmap.Reconstruction:\n        \"\"\"\n        Run complete SfM pipeline\n        \n        Args:\n            images_dir: Directory containing input images\n            output_dir: Directory for output files\n            image_paths: Optional list of specific image paths\n            \n        Returns:\n            pycolmap.Reconstruction: The reconstructed 3D model\n        \"\"\"\n        \n        if not self.config.validate_config():\n            raise ValueError(\"Invalid configuration\")\n        \n        logger.info(\"Starting SfM pipeline\")\n        self.config.print_config()\n        \n        # Create output directory\n        output_dir.mkdir(parents=True, exist_ok=True)\n        \n        # Get image paths if not provided\n        if image_paths is None:\n            image_paths = list(images_dir.glob(\"*.jpg\")) + \\\n                         list(images_dir.glob(\"*.png\")) + \\\n                         list(images_dir.glob(\"*.jpeg\"))\n            image_paths.sort()\n        \n        logger.info(f\"Processing {len(image_paths)} images\")\n        \n        # Step 1: Generate image pairs\n        pairs_file = self._generate_pairs(images_dir, image_paths, output_dir)\n        \n        # Step 2: Feature extraction and matching\n        feature_path, match_path = self._extract_and_match_features(\n            images_dir, output_dir, pairs_file\n        )\n        \n        # Step 3: 3D reconstruction\n        self.model = SfMReconstructor.reconstruct(\n            images_dir, pairs_file, feature_path, match_path, output_dir\n        )\n        \n        logger.info(\"SfM pipeline completed successfully\")\n        return self.model\n    \n    def _generate_pairs(\n        self, \n        images_dir: Path, \n        image_paths: List[Path], \n        output_dir: Path\n    ) -> Path:\n        \"\"\"Generate image pairs for matching\"\"\"\n        \n        # Use exhaustive pairs for small datasets\n        if (len(image_paths) <= self.config.config[\"max_images_exhaustive\"] or \n            self.config.config[\"use_exhaustive_pairs\"]):\n            \n            pairs_file = output_dir / \"pairs-exhaustive.txt\"\n            return ImagePairGenerator.generate_exhaustive_pairs(image_paths, pairs_file)\n        \n        # Use retrieval-based pairs for larger datasets\n        else:\n            return ImagePairGenerator.generate_retrieval_pairs(\n                images_dir,\n                output_dir,\n                self.config.config[\"retrieval_conf\"],\n                self.config.config[\"num_matched_pairs\"]\n            )\n    \n    def _extract_and_match_features(\n        self, \n        images_dir: Path, \n        output_dir: Path, \n        pairs_file: Path\n    ) -> Tuple[Path, Path]:\n        \"\"\"Extract features and perform matching\"\"\"\n        \n        if self.config.config[\"match_method\"] == \"dense\":\n            # Dense matching\n            return FeatureMatcher.match_dense(\n                pairs_file,\n                images_dir,\n                output_dir,\n                self.config.config[\"matcher_conf_dense\"]\n            )\n        \n        else:\n            # Feature-based matching\n            # First extract features\n            feature_path = FeatureExtractor.extract_features(\n                images_dir,\n                output_dir,\n                self.config.config[\"feature_conf\"]\n            )\n            \n            # Then match features\n            match_path = FeatureMatcher.match_features(\n                pairs_file,\n                feature_path,\n                output_dir,\n                self.config.config[\"matcher_conf_feature\"],\n                self.config.config[\"max_error\"],\n                self.config.config[\"cell_size\"]\n            )\n            \n            return feature_path, match_path\n    \n    def visualize_3d(self, show_points: bool = True, show_cameras: bool = True):\n        \"\"\"Visualize 3D reconstruction\"\"\"\n        if self.model is None:\n            logger.error(\"No model available. Run pipeline first.\")\n            return\n        \n        try:\n            fig = viz_3d.init_figure()\n            viz_3d.plot_reconstruction(\n                fig, \n                self.model, \n                color='rgba(255,0,0,0.5)', \n                name=\"reconstruction\", \n                points_rgb=True\n            )\n            fig.show()\n        except Exception as e:\n            logger.error(f\"3D visualization failed: {e}\")\n    \n    def visualize_2d(\n        self, \n        images_dir: Path, \n        color_by: str = \"visibility\", \n        n: int = 5\n    ):\n        \"\"\"Visualize 2D projections\"\"\"\n        if self.model is None:\n            logger.error(\"No model available. Run pipeline first.\")\n            return\n        \n        try:\n            visualization.visualize_sfm_2d(\n                self.model, \n                images_dir, \n                color_by=color_by, \n                n=n\n            )\n        except Exception as e:\n            logger.error(f\"2D visualization failed: {e}\")\n    \n    def get_camera_poses(self) -> Dict[str, Dict[str, np.ndarray]]:\n        \"\"\"Extract camera poses from reconstruction\"\"\"\n        if self.model is None:\n            logger.error(\"No model available. Run pipeline first.\")\n            return {}\n        \n        poses = {}\n        for image_id, image in self.model.images.items():\n            poses[image.name] = {\n                \"R\": image.cam_from_world.rotation.matrix(),\n                \"t\": np.array(image.cam_from_world.translation),\n                \"camera_id\": image.camera_id\n            }\n        \n        return poses\n    \n    def export_poses(self, output_file: Path, format: str = \"txt\"):\n        \"\"\"Export camera poses to file\"\"\"\n        poses = self.get_camera_poses()\n        \n        if format == \"txt\":\n            with open(output_file, 'w') as f:\n                f.write(\"image_name rotation_matrix translation_vector\\n\")\n                for image_name, pose in poses.items():\n                    R_str = \" \".join([str(x) for x in pose[\"R\"].flatten()])\n                    t_str = \" \".join([str(x) for x in pose[\"t\"]])\n                    f.write(f\"{image_name} {R_str} {t_str}\\n\")\n        \n        elif format == \"csv\":\n            with open(output_file, 'w') as f:\n                f.write(\"image_name,r11,r12,r13,r21,r22,r23,r31,r32,r33,tx,ty,tz\\n\")\n                for image_name, pose in poses.items():\n                    R_flat = pose[\"R\"].flatten()\n                    t = pose[\"t\"]\n                    f.write(f\"{image_name},{','.join([str(x) for x in R_flat])},{','.join([str(x) for x in t])}\\n\")\n        \n        logger.info(f\"Poses exported to {output_file}\")","metadata":{"_uuid":"c042b4c0-2396-49c3-9ea8-3193258ca2d9","_cell_guid":"145c4c2d-f677-4e2a-92d0-fbfc2d402d2d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-16T04:25:11.502723Z","iopub.execute_input":"2025-09-16T04:25:11.503017Z","iopub.status.idle":"2025-09-16T04:25:11.521796Z","shell.execute_reply.started":"2025-09-16T04:25:11.502987Z","shell.execute_reply":"2025-09-16T04:25:11.52088Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def arr_to_str(a: np.ndarray) -> str:\n    \"\"\"Convert array to semicolon-separated string\"\"\"\n    return \";\".join([str(x) for x in a.reshape(-1)])","metadata":{"_uuid":"eb77449b-cfb2-4eab-8bce-1805c625c2f7","_cell_guid":"479c8c41-8613-4890-8dfe-7206196d5e05","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:25:11.522909Z","iopub.execute_input":"2025-09-16T04:25:11.523181Z","iopub.status.idle":"2025-09-16T04:25:11.536167Z","shell.execute_reply.started":"2025-09-16T04:25:11.52316Z","shell.execute_reply":"2025-09-16T04:25:11.53519Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_kaggle_submission(\n    results: Dict,\n    data_dict: Dict,\n    base_path: Path,\n    output_file: Path = Path(\"submission.csv\")\n) -> None:\n    \"\"\"Create submission file in Kaggle format\"\"\"\n    \n    with open(output_file, \"w\") as f:\n        f.write(\"image_path,dataset,scene,rotation_matrix,translation_vector\\n\")\n        \n        for dataset in data_dict:\n            if dataset in results:\n                res = results[dataset]\n            else:\n                res = {}\n            \n            for scene in data_dict[dataset]:\n                if scene in res:\n                    scene_res = res[scene]\n                else:\n                    scene_res = {}\n                \n                for image_path in data_dict[dataset][scene]:\n                    if image_path in scene_res:\n                        R = scene_res[image_path][\"R\"]\n                        t = scene_res[image_path][\"t\"]\n                    else:\n                        R = np.eye(3)\n                        t = np.zeros(3)\n                    \n                    rel_path = image_path.relative_to(base_path)\n                    f.write(f\"{rel_path},{dataset},{scene},{arr_to_str(R)},{arr_to_str(t)}\\n\")\n    \n    #logger.info(f\"Submission file created: {output_file}\")","metadata":{"_uuid":"b778e9ec-d38d-4ba2-94ae-75c499b43bdb","_cell_guid":"519a30bb-54d2-4c2a-bc5d-d4d56b107d0a","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:25:11.538263Z","iopub.execute_input":"2025-09-16T04:25:11.538554Z","iopub.status.idle":"2025-09-16T04:25:11.547025Z","shell.execute_reply.started":"2025-09-16T04:25:11.538533Z","shell.execute_reply":"2025-09-16T04:25:11.546272Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from hloc import match_features\nprint(match_features.confs.keys())","metadata":{"_uuid":"4762c029-52fb-4230-b593-18495029ece6","_cell_guid":"3e674a6f-8e4b-4da1-a3e7-bba33a77e318","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:25:11.548101Z","iopub.execute_input":"2025-09-16T04:25:11.548416Z","iopub.status.idle":"2025-09-16T04:25:11.562294Z","shell.execute_reply.started":"2025-09-16T04:25:11.548388Z","shell.execute_reply":"2025-09-16T04:25:11.561428Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## SfM Pipeline: Feature Extraction, Pair Generation, and Matching\n\n- **Setup paths** and remove old outputs.\n- **Step 1:** Extract features using SuperPoint.\n- **Step 2:** Generate exhaustive image pairs.\n- **Step 3:** Match features using SuperPoint + LightGlue.\n","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nfrom hloc import extract_features, match_features, pairs_from_exhaustive\nimport os\nif (outputs / \"features.h5\").exists():\n    os.remove(outputs / \"features.h5\")\nif (outputs / \"matches.h5\").exists():\n    os.remove(outputs / \"matches.h5\")\n\nimages = Path(\"/kaggle/input/image-matching-challenge-2024/train/church/images\")\noutputs = Path(\"outputs/\")\nsfm_dir = outputs / \"sfm\"\nsfm_dir.mkdir(exist_ok=True, parents=True)\n\n# Step 1: feature extraction (SuperPoint)\nfeatures = extract_features.main(\n    extract_features.confs[\"superpoint_aachen\"],\n    image_dir=images,\n    feature_path=outputs / \"features.h5\"\n)\n\n# Step 2: image pairs (generate exhaustive pairs)\n# Step 2: image pairs (generate exhaustive pairs)\nimage_list = [p.name for p in images.iterdir() if p.suffix.lower() in [\".jpg\", \".jpeg\", \".png\"]]\nprint(\"Number of images found:\", len(image_list))\npairs = outputs / \"pairs.txt\"\npairs_from_exhaustive.main(pairs, image_list=image_list)\n\n\n# Step 3: matching (SuperPoint + LightGlue)\nmatches = match_features.main(\n    match_features.confs[\"superpoint+lightglue\"],\n    pairs=pairs,\n    features=features,\n    matches=outputs / \"matches.h5\"\n)","metadata":{"_uuid":"04d318f1-1246-468f-8df1-9f6793c81a72","_cell_guid":"04110abc-c9a5-4dac-84dc-0497164928da","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:25:11.563468Z","iopub.execute_input":"2025-09-16T04:25:11.563697Z","iopub.status.idle":"2025-09-16T04:33:29.293025Z","shell.execute_reply.started":"2025-09-16T04:25:11.563677Z","shell.execute_reply":"2025-09-16T04:33:29.292217Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Main Function: Running the SfM Pipeline\n\n- **Configure** the SfM pipeline parameters.\n- **Set input/output paths** for images and results.\n- **Run the pipeline** to reconstruct 3D scene:\n  - Extract features\n  - Generate image pairs\n  - Match features\n  - Perform 3D reconstruction\n- **Get and export camera poses** in TXT and CSV.\n- **Optional visualizations**: interactive 3D and 2D projections.\n","metadata":{}},{"cell_type":"code","source":"def main():\n    \"\"\"Example usage of the SfM pipeline on Kaggle\"\"\"\n\n    # Configuration\n    config = SfMConfiguration()\n    config.update_config(\n        match_method=\"feature\",                     # use feature pipeline\n        feature_conf=\"superpoint_aachen\",           # valid in your FEATURE_CONFIGS\n        matcher_conf_feature=\"superpoint+lightglue\",# valid in match_features.confs\n        use_exhaustive_pairs=True,                  # exhaustive pairing (ok for small scenes)\n        max_images_exhaustive=50\n    )\n\n    # Example paths (update as needed)\n    images_dir = Path(\"/kaggle/input/image-matching-challenge-2024/train/church/images\")\n    output_dir = Path(\"./outputs\")\n\n    # Initialize and run pipeline\n    pipeline = SfMPipeline(config)\n\n    try:\n        model = pipeline.run(images_dir, output_dir)\n\n        # Get camera poses\n        poses = pipeline.get_camera_poses()\n        print(f\"Reconstructed {len(poses)} camera poses\")\n\n        # Export poses\n        pipeline.export_poses(output_dir / \"poses.txt\", format=\"txt\")\n        pipeline.export_poses(output_dir / \"poses.csv\", format=\"csv\")\n\n        # Optional visualizations\n        pipeline.visualize_3d()\n        pipeline.visualize_2d(images_dir, color_by=\"visibility\", n=5)\n\n        logger.info(\"Pipeline completed successfully!\")\n\n    except Exception as e:\n        logger.error(f\"Pipeline failed: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    main()\n","metadata":{"_uuid":"7ff39bc4-5db1-4dfa-bbe6-de8f7d22db88","_cell_guid":"b7b5d14a-fa4f-4456-969a-aa74c663dd5e","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-09-16T04:38:59.871143Z","iopub.execute_input":"2025-09-16T04:38:59.871847Z","iopub.status.idle":"2025-09-16T04:53:25.849981Z","shell.execute_reply.started":"2025-09-16T04:38:59.87182Z","shell.execute_reply":"2025-09-16T04:53:25.849011Z"},"jupyter":{"outputs_hidden":false},"scrolled":true},"outputs":[],"execution_count":null}]}