├── .clang-format ├── .clang-tidy ├── .github └── workflows │ ├── build-new.yml │ └── clang-format-check.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── example.py ├── format.sh ├── package ├── install-colmap-centos.sh ├── install-colmap-macos.sh └── install-colmap-windows.ps1 ├── pycolmap ├── estimators │ ├── absolute_pose.h │ ├── alignment.h │ ├── bindings.h │ ├── essential_matrix.h │ ├── fundamental_matrix.h │ ├── generalized_absolute_pose.h │ ├── homography_matrix.h │ ├── triangulation.h │ └── two_view_geometry.h ├── feature │ └── sift.h ├── geometry │ ├── bindings.h │ └── homography_matrix.h ├── helpers.h ├── log_exceptions.h ├── main.cc ├── optim │ └── bindings.h ├── pipeline │ ├── bindings.h │ ├── extract_features.h │ ├── images.h │ ├── match_features.h │ ├── meshing.h │ ├── mvs.h │ └── sfm.h ├── pybind11_extension.h ├── scene │ ├── bindings.h │ ├── camera.h │ ├── correspondence_graph.h │ ├── database.h │ ├── image.h │ ├── point2D.h │ ├── point3D.h │ ├── reconstruction.h │ └── track.h ├── sfm │ ├── bindings.h │ ├── incremental_mapper.h │ └── incremental_triangulator.h └── utils.h └── pyproject.toml /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | BinPackArguments: false 3 | BinPackParameters: false 4 | DerivePointerAlignment: false 5 | IncludeBlocks: Regroup 6 | IncludeCategories: 7 | - Regex: '^"colmap' 8 | Priority: 1 9 | - Regex: '^"pycolmap' 10 | Priority: 2 11 | - Regex: '^<[[:alnum:]_]+>' 12 | Priority: 3 13 | - Regex: '".*' 14 | Priority: 4 15 | SortIncludes: true 16 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | Checks: > 2 | performance-*, 3 | concurrency-*, 4 | bugprone-*, 5 | -bugprone-easily-swappable-parameters, 6 | -bugprone-exception-escape, 7 | -bugprone-implicit-widening-of-multiplication-result, 8 | -bugprone-narrowing-conversions, 9 | -bugprone-reserved-identifier, 10 | -bugprone-unchecked-optional-access, 11 | cppcoreguidelines-virtual-class-destructor, 12 | google-explicit-constructor, 13 | google-build-using-namespace, 14 | readability-avoid-const-params-in-decls, 15 | clang-analyzer-core*, 16 | clang-analyzer-cplusplus*, 17 | WarningsAsErrors: '*' 18 | FormatStyle: 'file' 19 | User: 'user' 20 | -------------------------------------------------------------------------------- /.github/workflows/build-new.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish wheels 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ assigned, opened, synchronize, reopened ] 9 | release: 10 | types: [ published, edited ] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | name: Build on ${{ matrix.config.os }} ${{ matrix.config.arch }} 16 | runs-on: ${{ matrix.config.os }} 17 | strategy: 18 | matrix: 19 | config: [ 20 | {os: ubuntu-latest}, 21 | {os: macos-13, arch: x86_64}, 22 | {os: macos-13, arch: arm64}, 23 | {os: windows-latest}, 24 | ] 25 | env: 26 | COMPILER_CACHE_VERSION: 1 27 | COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache 28 | CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache 29 | CCACHE_BASEDIR: ${{ github.workspace }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/cache@v4 33 | id: cache-builds 34 | with: 35 | key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ github.run_id }}-${{ github.run_number }} 36 | restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }} 37 | path: ${{ env.COMPILER_CACHE_DIR }} 38 | - name: Set env (macOS) 39 | if: runner.os == 'macOS' 40 | run: | 41 | if [[ ${{ matrix.config.arch }} == "x86_64" ]]; then 42 | VCPKG_TARGET_TRIPLET="x64-osx" 43 | elif [[ ${{ matrix.config.arch }} == "arm64" ]]; then 44 | VCPKG_TARGET_TRIPLET="arm64-osx-release" 45 | else 46 | exit 1 47 | fi 48 | echo "VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" >> "$GITHUB_ENV" 49 | 50 | VCPKG_INSTALLATION_ROOT="/Users/runner/work/vcpkg" 51 | CMAKE_TOOLCHAIN_FILE="${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" 52 | CMAKE_OSX_ARCHITECTURES=${{ matrix.config.arch }} 53 | echo "VCPKG_INSTALLATION_ROOT=${VCPKG_INSTALLATION_ROOT}" >> "$GITHUB_ENV" 54 | echo "CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" >> "$GITHUB_ENV" 55 | echo "CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" >> "$GITHUB_ENV" 56 | 57 | # Fix: cibuildhweel cannot interpolate env variables. 58 | CONFIG_SETTINGS="cmake.define.CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" 59 | CONFIG_SETTINGS="${CONFIG_SETTINGS} cmake.define.VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" 60 | CONFIG_SETTINGS="${CONFIG_SETTINGS} cmake.define.CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" 61 | echo "CIBW_CONFIG_SETTINGS_MACOS=${CONFIG_SETTINGS}" >> "$GITHUB_ENV" 62 | 63 | # vcpkg binary caching 64 | VCPKG_CACHE_DIR="${COMPILER_CACHE_DIR}/vcpkg" 65 | VCPKG_BINARY_SOURCES="clear;files,${VCPKG_CACHE_DIR},readwrite" 66 | echo "VCPKG_BINARY_SOURCES=${VCPKG_BINARY_SOURCES}" >> "$GITHUB_ENV" 67 | - name: Set env (Windows) 68 | if: runner.os == 'Windows' 69 | shell: pwsh 70 | run: | 71 | $VCPKG_INSTALLATION_ROOT="${{ github.workspace }}/vcpkg" 72 | echo "VCPKG_INSTALLATION_ROOT=${VCPKG_INSTALLATION_ROOT}" >> "${env:GITHUB_ENV}" 73 | $CMAKE_TOOLCHAIN_FILE = "${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" 74 | echo "CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" >> "${env:GITHUB_ENV}" 75 | $VCPKG_TARGET_TRIPLET = "x64-windows" 76 | echo "VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" >> "${env:GITHUB_ENV}" 77 | 78 | # Fix: cibuildhweel cannot interpolate env variables. 79 | $CMAKE_TOOLCHAIN_FILE = $CMAKE_TOOLCHAIN_FILE.replace('\', '/') 80 | $CONFIG_SETTINGS = "cmake.define.CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" 81 | $CONFIG_SETTINGS = "${CONFIG_SETTINGS} cmake.define.VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" 82 | echo "CIBW_CONFIG_SETTINGS_WINDOWS=${CONFIG_SETTINGS}" >> "${env:GITHUB_ENV}" 83 | $CIBW_REPAIR_WHEEL_COMMAND = "delvewheel repair -v --add-path ${VCPKG_INSTALLATION_ROOT}/installed/${VCPKG_TARGET_TRIPLET}/bin -w {dest_dir} {wheel}" 84 | echo "CIBW_REPAIR_WHEEL_COMMAND_WINDOWS=${CIBW_REPAIR_WHEEL_COMMAND}" >> "${env:GITHUB_ENV}" 85 | 86 | # vcpkg binary caching 87 | $VCPKG_CACHE_DIR = "${env:COMPILER_CACHE_DIR}/vcpkg" 88 | $VCPKG_BINARY_SOURCES = "clear;files,${VCPKG_CACHE_DIR},readwrite" 89 | echo "VCPKG_BINARY_SOURCES=${VCPKG_BINARY_SOURCES}" >> "${env:GITHUB_ENV}" 90 | - name: Set env (Ubuntu) 91 | if: runner.os == 'Linux' 92 | run: | 93 | VCPKG_TARGET_TRIPLET="x64-linux-release" 94 | echo "VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" >> "$GITHUB_ENV" 95 | 96 | VCPKG_INSTALLATION_ROOT="${{ github.workspace }}/vcpkg" 97 | CMAKE_TOOLCHAIN_FILE="${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" 98 | echo "VCPKG_INSTALLATION_ROOT=${VCPKG_INSTALLATION_ROOT}" >> "$GITHUB_ENV" 99 | echo "CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" >> "$GITHUB_ENV" 100 | 101 | # Fix: cibuildhweel cannot interpolate env variables. 102 | CONFIG_SETTINGS="cmake.define.CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" 103 | CONFIG_SETTINGS="${CONFIG_SETTINGS} cmake.define.VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" 104 | echo "CIBW_CONFIG_SETTINGS_LINUX=${CONFIG_SETTINGS}" >> "$GITHUB_ENV" 105 | 106 | # Remap caching paths to the container 107 | CONTAINER_COMPILER_CACHE_DIR="/compiler-cache" 108 | CIBW_CONTAINER_ENGINE="docker; create_args: -v ${COMPILER_CACHE_DIR}:${CONTAINER_COMPILER_CACHE_DIR}" 109 | echo "CIBW_CONTAINER_ENGINE=${CIBW_CONTAINER_ENGINE}" >> "$GITHUB_ENV" 110 | echo "CONTAINER_COMPILER_CACHE_DIR=${CONTAINER_COMPILER_CACHE_DIR}" >> "$GITHUB_ENV" 111 | echo "CCACHE_DIR=${CONTAINER_COMPILER_CACHE_DIR}/ccache" >> "$GITHUB_ENV" 112 | echo "CCACHE_BASEDIR=/project" >> "$GITHUB_ENV" 113 | 114 | # vcpkg binary caching 115 | VCPKG_CACHE_DIR="${CONTAINER_COMPILER_CACHE_DIR}/vcpkg" 116 | VCPKG_BINARY_SOURCES="clear;files,${VCPKG_CACHE_DIR},readwrite" 117 | echo "VCPKG_BINARY_SOURCES=${VCPKG_BINARY_SOURCES}" >> "$GITHUB_ENV" 118 | 119 | CIBW_ENVIRONMENT_PASS_LINUX="VCPKG_TARGET_TRIPLET VCPKG_INSTALLATION_ROOT CMAKE_TOOLCHAIN_FILE VCPKG_BINARY_SOURCES CONTAINER_COMPILER_CACHE_DIR CCACHE_DIR CCACHE_BASEDIR" 120 | echo "CIBW_ENVIRONMENT_PASS_LINUX=${CIBW_ENVIRONMENT_PASS_LINUX}" >> "$GITHUB_ENV" 121 | - name: Build wheels 122 | uses: pypa/cibuildwheel@v2.16.2 123 | env: 124 | CIBW_ARCHS_MACOS: ${{ matrix.config.arch }} 125 | - name: Archive wheels 126 | uses: actions/upload-artifact@v4 127 | with: 128 | name: pycolmap-${{ matrix.config.os }}-${{ matrix.config.arch }} 129 | path: wheelhouse/pycolmap-*.whl 130 | 131 | pypi-publish: 132 | name: Publish wheels to PyPI 133 | needs: build 134 | runs-on: ubuntu-latest 135 | # We publish the wheel to pypi when a new tag is pushed, 136 | # either by creating a new GitHub release or explictly with `git tag` 137 | if: ${{ github.event_name == 'release' || startsWith(github.ref, 'refs/tags') }} 138 | steps: 139 | - name: Download wheels 140 | uses: actions/download-artifact@v4 141 | with: 142 | path: ./artifacts/ 143 | - name: Move wheels 144 | run: mkdir ./wheelhouse && mv ./artifacts/**/*.whl ./wheelhouse/ 145 | - name: Publish package 146 | uses: pypa/gh-action-pypi-publish@release/v1 147 | with: 148 | skip_existing: true 149 | user: __token__ 150 | password: ${{ secrets.PYPI_API_TOKEN }} 151 | packages_dir: ./wheelhouse/ 152 | -------------------------------------------------------------------------------- /.github/workflows/clang-format-check.yml: -------------------------------------------------------------------------------- 1 | name: clang-format Check 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: [ assigned, opened, synchronize, reopened ] 8 | jobs: 9 | formatting-check: 10 | name: Formatting Check 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Run clang-format style check for C/C++/Protobuf programs. 15 | uses: jidicula/clang-format-action@v4.11.0 16 | with: 17 | clang-format-version: '14' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.so 3 | *.egg-info/ 4 | build/ 5 | .cache/ 6 | example/ 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colmap/pycolmap/b6627db2266f098c21c3d7e4b7844b4b90d8e02d/.gitmodules -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(${SKBUILD_PROJECT_NAME} VERSION ${SKBUILD_PROJECT_VERSION}) 3 | 4 | set(CMAKE_CUDA_ARCHITECTURES "native") 5 | if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 6 | # Some fixes for the Glog library. 7 | add_definitions("-DGLOG_NO_ABBREVIATED_SEVERITIES") 8 | add_definitions("-DGL_GLEXT_PROTOTYPES") 9 | add_definitions("-DNOMINMAX") 10 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc") 11 | # Enable object level parallel builds in Visual Studio. 12 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") 13 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") 14 | endif() 15 | 16 | 17 | find_package(colmap 3.9.1 REQUIRED) 18 | 19 | find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) 20 | 21 | find_package(pybind11 REQUIRED) 22 | 23 | pybind11_add_module(pycolmap pycolmap/main.cc) 24 | target_include_directories(pycolmap PRIVATE ${PROJECT_SOURCE_DIR}) 25 | target_link_libraries(pycolmap PRIVATE colmap::colmap freeimage::FreeImage glog::glog) 26 | target_compile_definitions(pycolmap PRIVATE VERSION_INFO="${PROJECT_VERSION}") 27 | install(TARGETS pycolmap LIBRARY DESTINATION .) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD-3-Clause 2 | 3 | Copyright (c) 2020, Mihai Dusmanu . 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ⚠️ The development of PyCOLMAP has moved to the COLMAP repository. ⚠️
PyCOLMAP remains available on PyPi. This repository will be archived soon. 4 |
5 |

6 | 7 | # Python bindings for COLMAP 8 | 9 | This repository exposes to Python most capabilities of [COLMAP](https://colmap.github.io/) for Structure-from-Motion and Multiview-stereo, such as reconstruction pipelines & objects and geometric estimators. 10 | 11 | ## Installation 12 | 13 | Wheels for Python 8/9/10 on Linux, macOS 10/11/12 (both Intel and Apple Silicon), and Windows can be installed using pip: 14 | ```bash 15 | pip install pycolmap 16 | ``` 17 | 18 | The wheels are automatically built and pushed to [PyPI](https://pypi.org/project/pycolmap/) at each release. They are currently not built with CUDA support, which requires building from source. 19 | 20 |
21 | [Building PyCOLMAP from source - click to expand] 22 | 23 | 1. Install COLMAP from source following [the official guide](https://colmap.github.io/install.html). Use COLMAP 3.8 or 3.9.1 for PyCOLMAP 0.4.0 or 0.5.0/0.6.0. 24 | 25 | 4. Clone the PyCOLMAP repository: 26 | ```bash 27 | git clone -b 0.6.0 https://github.com/colmap/pycolmap.git 28 | cd pycolmap 29 | ``` 30 | 31 | 3. Build: 32 | - On Linux and macOS: 33 | ```bash 34 | python -m pip install . 35 | ``` 36 | - On Windows, after installing COLMAP [via VCPKG](https://colmap.github.io/install.html), run in powershell: 37 | ```powershell 38 | py -m pip install . ` 39 | --cmake.define.CMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" ` 40 | --cmake.define.VCPKG_TARGET_TRIPLET="x64-windows" 41 | ``` 42 | 43 |
44 | 45 | ## Reconstruction pipeline 46 | 47 | PyCOLMAP provides bindings for multiple steps of the standard reconstruction pipeline: 48 | 49 | - extracting and matching SIFT features 50 | - importing an image folder into a COLMAP database 51 | - inferring the camera parameters from the EXIF metadata of an image file 52 | - running two-view geometric verification of matches on a COLMAP database 53 | - triangulating points into an existing COLMAP model 54 | - running incremental reconstruction from a COLMAP database 55 | - dense reconstruction with multi-view stereo 56 | 57 | Sparse & Dense reconstruction from a folder of images can be performed with: 58 | ```python 59 | output_path: pathlib.Path 60 | image_dir: pathlib.Path 61 | 62 | output_path.mkdir() 63 | mvs_path = output_path / "mvs" 64 | database_path = output_path / "database.db" 65 | 66 | pycolmap.extract_features(database_path, image_dir) 67 | pycolmap.match_exhaustive(database_path) 68 | maps = pycolmap.incremental_mapping(database_path, image_dir, output_path) 69 | maps[0].write(output_path) 70 | # dense reconstruction 71 | pycolmap.undistort_images(mvs_path, output_path, image_dir) 72 | pycolmap.patch_match_stereo(mvs_path) # requires compilation with CUDA 73 | pycolmap.stereo_fusion(mvs_path / "dense.ply", mvs_path) 74 | ``` 75 | 76 | PyCOLMAP can leverage the GPU for feature extraction, matching, and multi-view stereo if COLMAP was compiled with CUDA support. 77 | Similarly, PyCOLMAP can run Delauney Triangulation if COLMAP was compiled with CGAL support. 78 | This requires to build the package from source and is not available with the PyPI wheels. 79 | 80 | All of the above steps are easily configurable with python dicts which are recursively merged into 81 | their respective defaults, for example: 82 | ```python 83 | pycolmap.extract_features(database_path, image_dir, sift_options={"max_num_features": 512}) 84 | # equivalent to 85 | ops = pycolmap.SiftExtractionOptions() 86 | ops.max_num_features = 512 87 | pycolmap.extract_features(database_path, image_dir, sift_options=ops) 88 | ``` 89 | 90 | To list available options and their default parameters: 91 | 92 | ```python 93 | help(pycolmap.SiftExtractionOptions) 94 | ``` 95 | 96 | For another example of usage, see [`example.py`](./example.py) or [`hloc/reconstruction.py`](https://github.com/cvg/Hierarchical-Localization/blob/master/hloc/reconstruction.py). 97 | 98 | ## Reconstruction object 99 | 100 | We can load and manipulate an existing COLMAP 3D reconstruction: 101 | 102 | ```python 103 | import pycolmap 104 | reconstruction = pycolmap.Reconstruction("path/to/reconstruction/dir") 105 | print(reconstruction.summary()) 106 | 107 | for image_id, image in reconstruction.images.items(): 108 | print(image_id, image) 109 | 110 | for point3D_id, point3D in reconstruction.points3D.items(): 111 | print(point3D_id, point3D) 112 | 113 | for camera_id, camera in reconstruction.cameras.items(): 114 | print(camera_id, camera) 115 | 116 | reconstruction.write("path/to/reconstruction/dir/") 117 | ``` 118 | 119 | The object API mirrors the COLMAP C++ library. The bindings support many other operations, for example: 120 | 121 | - projecting a 3D point into an image with arbitrary camera model: 122 | ```python 123 | uv = camera.img_from_cam(image.cam_from_world * point3D.xyz) 124 | ``` 125 | 126 | - aligning two 3D reconstructions by their camera poses: 127 | ```python 128 | rec2_from_rec1 = pycolmap.align_reconstructions_via_reprojections(reconstruction1, reconstrution2) 129 | reconstruction1.transform(rec2_from_rec1) 130 | print(rec2_from_rec1.scale, rec2_from_rec1.rotation, rec2_from_rec1.translation) 131 | ``` 132 | 133 | - exporting reconstructions to text, PLY, or other formats: 134 | ```python 135 | reconstruction.write_text("path/to/new/reconstruction/dir/") # text format 136 | reconstruction.export_PLY("rec.ply") # PLY format 137 | ``` 138 | 139 | ## Estimators 140 | 141 | We provide robust RANSAC-based estimators for absolute camera pose (single-camera and multi-camera-rig), essential matrix, fundamental matrix, homography, and two-view relative pose for calibrated cameras. 142 | 143 | All RANSAC and estimation parameters are exposed as objects that behave similarly as Python dataclasses. The RANSAC options are described in [`colmap/optim/ransac.h`](https://github.com/colmap/colmap/blob/main/src/colmap/optim/ransac.h#L43-L72) and their default values are: 144 | 145 | ```python 146 | ransac_options = pycolmap.RANSACOptions( 147 | max_error=4.0, # for example the reprojection error in pixels 148 | min_inlier_ratio=0.01, 149 | confidence=0.9999, 150 | min_num_trials=1000, 151 | max_num_trials=100000, 152 | ) 153 | ``` 154 | 155 | ### Absolute pose estimation 156 | 157 | For instance, to estimate the absolute pose of a query camera given 2D-3D correspondences: 158 | ```python 159 | # Parameters: 160 | # - points2D: Nx2 array; pixel coordinates 161 | # - points3D: Nx3 array; world coordinates 162 | # - camera: pycolmap.Camera 163 | # Optional parameters: 164 | # - estimation_options: dict or pycolmap.AbsolutePoseEstimationOptions 165 | # - refinement_options: dict or pycolmap.AbsolutePoseRefinementOptions 166 | answer = pycolmap.absolute_pose_estimation(points2D, points3D, camera) 167 | # Returns: dictionary of estimation outputs or None if failure 168 | ``` 169 | 170 | 2D and 3D points are passed as Numpy arrays or lists. The options are defined in [`estimators/absolute_pose.cc`](./pycolmap/estimators/absolute_pose.h#L100-L122) and can be passed as regular (nested) Python dictionaries: 171 | 172 | ```python 173 | pycolmap.absolute_pose_estimation( 174 | points2D, points3D, camera, 175 | estimation_options=dict(ransac=dict(max_error=12.0)), 176 | refinement_options=dict(refine_focal_length=True), 177 | ) 178 | ``` 179 | 180 | ### Absolute Pose Refinement 181 | 182 | ```python 183 | # Parameters: 184 | # - cam_from_world: pycolmap.Rigid3d, initial pose 185 | # - points2D: Nx2 array; pixel coordinates 186 | # - points3D: Nx3 array; world coordinates 187 | # - inlier_mask: array of N bool; inlier_mask[i] is true if correpondence i is an inlier 188 | # - camera: pycolmap.Camera 189 | # Optional parameters: 190 | # - refinement_options: dict or pycolmap.AbsolutePoseRefinementOptions 191 | answer = pycolmap.pose_refinement(cam_from_world, points2D, points3D, inlier_mask, camera) 192 | # Returns: dictionary of refinement outputs or None if failure 193 | ``` 194 | 195 | ### Essential matrix estimation 196 | 197 | ```python 198 | # Parameters: 199 | # - points1: Nx2 array; 2D pixel coordinates in image 1 200 | # - points2: Nx2 array; 2D pixel coordinates in image 2 201 | # - camera1: pycolmap.Camera of image 1 202 | # - camera2: pycolmap.Camera of image 2 203 | # Optional parameters: 204 | # - options: dict or pycolmap.RANSACOptions (default inlier threshold is 4px) 205 | answer = pycolmap.essential_matrix_estimation(points1, points2, camera1, camera2) 206 | # Returns: dictionary of estimation outputs or None if failure 207 | ``` 208 | 209 | ### Fundamental matrix estimation 210 | 211 | ```python 212 | answer = pycolmap.fundamental_matrix_estimation( 213 | points1, 214 | points2, 215 | [options], # optional dict or pycolmap.RANSACOptions 216 | ) 217 | ``` 218 | 219 | ### Homography estimation 220 | 221 | ```python 222 | answer = pycolmap.homography_matrix_estimation( 223 | points1, 224 | points2, 225 | [options], # optional dict or pycolmap.RANSACOptions 226 | ) 227 | ``` 228 | 229 | ### Two-view geometry estimation 230 | 231 | COLMAP can also estimate a relative pose between two calibrated cameras by estimating both E and H and accounting for the degeneracies of each model. 232 | 233 | ```python 234 | # Parameters: 235 | # - camera1: pycolmap.Camera of image 1 236 | # - points1: Nx2 array; 2D pixel coordinates in image 1 237 | # - camera2: pycolmap.Camera of image 2 238 | # - points2: Nx2 array; 2D pixel coordinates in image 2 239 | # Optional parameters: 240 | # - matches: Nx2 integer array; correspondences across images 241 | # - options: dict or pycolmap.TwoViewGeometryOptions 242 | answer = pycolmap.estimate_calibrated_two_view_geometry(camera1, points1, camera2, points2) 243 | # Returns: pycolmap.TwoViewGeometry 244 | ``` 245 | 246 | The `TwoViewGeometryOptions` control how each model is selected. The output structure contains the geometric model, inlier matches, the relative pose (if `options.compute_relative_pose=True`), and the type of camera configuration, which is an instance of the enum `pycolmap.TwoViewGeometryConfiguration`. 247 | 248 | ### Camera argument 249 | 250 | Some estimators expect a COLMAP camera object, which can be created as follow: 251 | 252 | ```python 253 | camera = pycolmap.Camera( 254 | model=camera_model_name_or_id, 255 | width=width, 256 | height=height, 257 | params=params, 258 | ) 259 | ``` 260 | 261 | The different camera models and their extra parameters are defined in [`colmap/src/colmap/sensor/models.h`](https://github.com/colmap/colmap/blob/main/src/colmap/sensor/models.h). For example for a pinhole camera: 262 | 263 | ```python 264 | camera = pycolmap.Camera( 265 | model='SIMPLE_PINHOLE', 266 | width=width, 267 | height=height, 268 | params=[focal_length, cx, cy], 269 | ) 270 | ``` 271 | 272 | Alternatively, we can also pass a camera dictionary: 273 | 274 | ```python 275 | camera_dict = { 276 | 'model': COLMAP_CAMERA_MODEL_NAME_OR_ID, 277 | 'width': IMAGE_WIDTH, 278 | 'height': IMAGE_HEIGHT, 279 | 'params': EXTRA_CAMERA_PARAMETERS_LIST 280 | } 281 | ``` 282 | 283 | 284 | ## SIFT feature extraction 285 | 286 | ```python 287 | import numpy as np 288 | import pycolmap 289 | from PIL import Image, ImageOps 290 | 291 | # Input should be grayscale image with range [0, 1]. 292 | img = Image.open('image.jpg').convert('RGB') 293 | img = ImageOps.grayscale(img) 294 | img = np.array(img).astype(np.float) / 255. 295 | 296 | # Optional parameters: 297 | # - options: dict or pycolmap.SiftExtractionOptions 298 | # - device: default pycolmap.Device.auto uses the GPU if available 299 | sift = pycolmap.Sift() 300 | 301 | # Parameters: 302 | # - image: HxW float array 303 | keypoints, descriptors = sift.extract(img) 304 | # Returns: 305 | # - keypoints: Nx4 array; format: x (j), y (i), scale, orientation 306 | # - descriptors: Nx128 array; L2-normalized descriptors 307 | ``` 308 | 309 | ## TODO 310 | 311 | - [ ] Add documentation 312 | - [ ] Add more detailed examples 313 | - [ ] Add unit tests for reconstruction bindings 314 | 315 | Created and maintained by [Mihai Dusmanu](https://github.com/mihaidusmanu/), [Philipp Lindenberger](https://github.com/Phil26AT), [John Lambert](https://github.com/johnwlambert), [Paul-Edouard Sarlin](https://psarlin.com/), and other contributors. 316 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import urllib.request 3 | import zipfile 4 | from pathlib import Path 5 | 6 | import enlighten 7 | 8 | import pycolmap 9 | from pycolmap import logging 10 | 11 | 12 | def run(): 13 | output_path = Path("example/") 14 | image_path = output_path / "Fountain/images" 15 | database_path = output_path / "database.db" 16 | sfm_path = output_path / "sfm" 17 | 18 | output_path.mkdir(exist_ok=True) 19 | logging.set_log_destination(logging.INFO, output_path / "INFO.log.") # + time 20 | 21 | data_url = "https://cvg-data.inf.ethz.ch/local-feature-evaluation-schoenberger2017/Strecha-Fountain.zip" 22 | if not image_path.exists(): 23 | logging.info("Downloading the data.") 24 | zip_path = output_path / "data.zip" 25 | urllib.request.urlretrieve(data_url, zip_path) 26 | with zipfile.ZipFile(zip_path, "r") as fid: 27 | fid.extractall(output_path) 28 | logging.info(f"Data extracted to {output_path}.") 29 | 30 | if database_path.exists(): 31 | database_path.unlink() 32 | pycolmap.extract_features(database_path, image_path) 33 | pycolmap.match_exhaustive(database_path) 34 | num_images = pycolmap.Database(database_path).num_images 35 | 36 | if sfm_path.exists(): 37 | shutil.rmtree(sfm_path) 38 | sfm_path.mkdir(exist_ok=True) 39 | 40 | with enlighten.Manager() as manager: 41 | with manager.counter(total=num_images, desc="Images registered:") as pbar: 42 | pbar.update(0, force=True) 43 | recs = pycolmap.incremental_mapping( 44 | database_path, 45 | image_path, 46 | sfm_path, 47 | initial_image_pair_callback=lambda: pbar.update(2), 48 | next_image_callback=lambda: pbar.update(1), 49 | ) 50 | for idx, rec in recs.items(): 51 | logging.info(f"#{idx} {rec.summary()}") 52 | 53 | 54 | if __name__ == "__main__": 55 | run() 56 | -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | git ls-tree --full-tree -r --name-only HEAD . | grep ".*\(\.cc\|\.h\|\.hpp\|\.cpp\|\.cu\)$" | xargs clang-format -i 2 | -------------------------------------------------------------------------------- /package/install-colmap-centos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | uname -a 4 | CURRDIR=$(pwd) 5 | 6 | yum install -y gcc gcc-c++ ninja-build curl zip unzip tar 7 | 8 | # ccache shipped by CentOS is too old so we download and cache it. 9 | COMPILER_TOOLS_DIR="${CONTAINER_COMPILER_CACHE_DIR}/bin" 10 | mkdir -p ${COMPILER_TOOLS_DIR} 11 | if [ ! -f "${COMPILER_TOOLS_DIR}/ccache" ]; then 12 | FILE="ccache-4.9-linux-x86_64" 13 | curl -sSLO https://github.com/ccache/ccache/releases/download/v4.9/${FILE}.tar.xz 14 | tar -xf ${FILE}.tar.xz 15 | cp ${FILE}/ccache ${COMPILER_TOOLS_DIR} 16 | fi 17 | export PATH="${COMPILER_TOOLS_DIR}:${PATH}" 18 | ccache --version 19 | ccache --help 20 | 21 | git clone https://github.com/microsoft/vcpkg ${VCPKG_INSTALLATION_ROOT} 22 | cd ${VCPKG_INSTALLATION_ROOT} 23 | git checkout ${VCPKG_COMMIT_ID} 24 | ./bootstrap-vcpkg.sh 25 | ./vcpkg install --recurse --clean-after-build --triplet=${VCPKG_TARGET_TRIPLET} \ 26 | boost-algorithm \ 27 | boost-filesystem \ 28 | boost-graph \ 29 | boost-heap \ 30 | boost-program-options \ 31 | boost-property-map \ 32 | boost-property-tree \ 33 | boost-regex \ 34 | boost-system \ 35 | ceres[lapack,suitesparse] \ 36 | eigen3 \ 37 | flann \ 38 | jasper[core] \ 39 | freeimage \ 40 | metis \ 41 | gflags \ 42 | glog \ 43 | gtest \ 44 | sqlite3 45 | # We force the core option of jasper to disable the unwanted opengl option. 46 | ./vcpkg integrate install 47 | 48 | cd ${CURRDIR} 49 | git clone https://github.com/colmap/colmap.git 50 | cd colmap 51 | git checkout ${COLMAP_COMMIT_ID} 52 | mkdir build && cd build 53 | CXXFLAGS="-fPIC" CFLAGS="-fPIC" cmake .. -GNinja \ 54 | -DCUDA_ENABLED=OFF \ 55 | -DCGAL_ENABLED=OFF \ 56 | -DGUI_ENABLED=OFF \ 57 | -DCMAKE_BUILD_TYPE=Release \ 58 | -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" \ 59 | -DVCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET} \ 60 | -DCMAKE_EXE_LINKER_FLAGS_INIT="-ldl" 61 | ninja install 62 | 63 | ccache --show-stats --verbose 64 | ccache --evict-older-than 1d 65 | ccache --show-stats --verbose 66 | -------------------------------------------------------------------------------- /package/install-colmap-macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x -e 3 | CURRDIR=$(pwd) 4 | 5 | # See https://github.com/actions/setup-python/issues/577 6 | find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete 7 | # See https://github.com/actions/setup-python/issues/577#issuecomment-1500828576 8 | rm /usr/local/bin/go || true 9 | rm /usr/local/bin/gofmt || true 10 | 11 | # Updating requires Xcode 14.0, which cannot be installed on macOS 11. 12 | brew remove swiftlint 13 | brew remove node@18 14 | 15 | brew update 16 | brew install git cmake ninja llvm ccache 17 | 18 | # When building lapack-reference, vcpkg/cmake looks for gfortran. 19 | ln -s $(which gfortran-13) "$(dirname $(which gfortran-13))/gfortran" 20 | 21 | git clone https://github.com/microsoft/vcpkg ${VCPKG_INSTALLATION_ROOT} 22 | cd ${VCPKG_INSTALLATION_ROOT} 23 | git checkout ${VCPKG_COMMIT_ID} 24 | ./bootstrap-vcpkg.sh 25 | ./vcpkg install --recurse --clean-after-build --triplet=${VCPKG_TARGET_TRIPLET} \ 26 | boost-algorithm \ 27 | boost-filesystem \ 28 | boost-graph \ 29 | boost-heap \ 30 | boost-program-options \ 31 | boost-property-map \ 32 | boost-property-tree \ 33 | boost-regex \ 34 | boost-system \ 35 | ceres[lapack,suitesparse] \ 36 | eigen3 \ 37 | flann \ 38 | freeimage \ 39 | metis \ 40 | gflags \ 41 | glog \ 42 | gtest \ 43 | sqlite3 44 | ./vcpkg integrate install 45 | 46 | cd ${CURRDIR} 47 | git clone https://github.com/colmap/colmap.git 48 | cd colmap 49 | git checkout ${COLMAP_COMMIT_ID} 50 | mkdir build && cd build 51 | export ARCHFLAGS="-arch ${CIBW_ARCHS_MACOS}" 52 | cmake .. -GNinja -DGUI_ENABLED=OFF \ 53 | -DCUDA_ENABLED=OFF \ 54 | -DCGAL_ENABLED=OFF \ 55 | -DCMAKE_BUILD_TYPE=Release \ 56 | -DCCACHE_ENABLED=ON \ 57 | -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" \ 58 | -DVCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET} \ 59 | -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} \ 60 | `if [[ ${CIBW_ARCHS_MACOS} == "arm64" ]]; then echo "-DSIMD_ENABLED=OFF"; fi` 61 | ninja install 62 | 63 | ccache --show-stats --verbose 64 | ccache --evict-older-than 1d 65 | ccache --show-stats --verbose 66 | -------------------------------------------------------------------------------- /package/install-colmap-windows.ps1: -------------------------------------------------------------------------------- 1 | $CURRDIR = $PWD 2 | 3 | $COMPILER_TOOLS_DIR = "${env:COMPILER_CACHE_DIR}/bin" 4 | New-Item -ItemType Directory -Force -Path ${COMPILER_TOOLS_DIR} 5 | $env:Path = "${COMPILER_TOOLS_DIR};" + $env:Path 6 | 7 | $NINJA_PATH = "${COMPILER_TOOLS_DIR}/ninja.exe" 8 | If (!(Test-Path -path ${NINJA_PATH} -PathType Leaf)) { 9 | $zip_path = "${env:TEMP}/ninja.zip" 10 | $url = "https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-win.zip" 11 | curl.exe -L -o ${zip_path} ${url} 12 | Expand-Archive -LiteralPath ${zip_path} -DestinationPath ${COMPILER_TOOLS_DIR} 13 | Remove-Item ${zip_path} 14 | } 15 | If (!(Test-Path -path "${COMPILER_TOOLS_DIR}/ccache.exe" -PathType Leaf)) { 16 | # For some reason this CI runs an earlier PowerShell version that is 17 | # not compatible with colmap/.azure-pipelines/install-ccache.ps1 18 | $folder = "ccache-4.8-windows-x86_64" 19 | $url = "https://github.com/ccache/ccache/releases/download/v4.8/${folder}.zip" 20 | $zip_path = "${env:TEMP}/${folder}.zip" 21 | $folder_path = "${env:TEMP}/${folder}" 22 | curl.exe -L -o ${zip_path} ${url} 23 | Expand-Archive -LiteralPath ${zip_path} -DestinationPath "$env:TEMP" 24 | Move-Item -Force "${folder_path}/ccache.exe" ${COMPILER_TOOLS_DIR} 25 | Remove-Item ${zip_path} 26 | Remove-Item -Recurse ${folder_path} 27 | } 28 | 29 | cd ${CURRDIR} 30 | git clone https://github.com/microsoft/vcpkg ${env:VCPKG_INSTALLATION_ROOT} 31 | cd ${env:VCPKG_INSTALLATION_ROOT} 32 | git checkout "${env:VCPKG_COMMIT_ID}" 33 | ./bootstrap-vcpkg.bat 34 | 35 | cd ${CURRDIR} 36 | git clone https://github.com/colmap/colmap.git 37 | cd colmap 38 | git checkout "${env:COLMAP_COMMIT_ID}" 39 | 40 | & "./scripts/shell/enter_vs_dev_shell.ps1" 41 | 42 | [System.Collections.ArrayList]$DEPS = Get-Content -Path ".azure-pipelines/build-windows-vcpkg.txt" 43 | $DEPS.Remove("cgal") 44 | $DEPS.Remove("qt5-base") 45 | $DEPS.Remove("glew") 46 | & "${env:VCPKG_INSTALLATION_ROOT}/vcpkg.exe" install --recurse --clean-after-build @DEPS 47 | & "${env:VCPKG_INSTALLATION_ROOT}/vcpkg.exe" integrate install 48 | 49 | mkdir build 50 | cd build 51 | cmake .. ` 52 | -GNinja ` 53 | -DCMAKE_MAKE_PROGRAM="${NINJA_PATH}" ` 54 | -DCMAKE_BUILD_TYPE="Release" ` 55 | -DCUDA_ENABLED="OFF" ` 56 | -DCGAL_ENABLED="OFF" ` 57 | -DGUI_ENABLED="OFF" ` 58 | -DCMAKE_TOOLCHAIN_FILE="${env:CMAKE_TOOLCHAIN_FILE}" ` 59 | -DVCPKG_TARGET_TRIPLET="${env:VCPKG_TARGET_TRIPLET}" 60 | & ${NINJA_PATH} install 61 | 62 | ccache --show-stats --verbose 63 | ccache --evict-older-than 1d 64 | ccache --show-stats --verbose 65 | -------------------------------------------------------------------------------- /pycolmap/estimators/absolute_pose.h: -------------------------------------------------------------------------------- 1 | #include "colmap/estimators/pose.h" 2 | #include "colmap/geometry/rigid3.h" 3 | #include "colmap/math/random.h" 4 | #include "colmap/scene/camera.h" 5 | 6 | #include "pycolmap/helpers.h" 7 | #include "pycolmap/log_exceptions.h" 8 | #include "pycolmap/utils.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | using namespace colmap; 15 | using namespace pybind11::literals; 16 | namespace py = pybind11; 17 | 18 | py::object PyEstimateAndRefineAbsolutePose( 19 | const std::vector& points2D, 20 | const std::vector& points3D, 21 | Camera& camera, 22 | const AbsolutePoseEstimationOptions& estimation_options, 23 | const AbsolutePoseRefinementOptions& refinement_options, 24 | const bool return_covariance) { 25 | SetPRNGSeed(0); 26 | THROW_CHECK_EQ(points2D.size(), points3D.size()); 27 | py::object failure = py::none(); 28 | py::gil_scoped_release release; 29 | 30 | // Absolute pose estimation. 31 | Rigid3d cam_from_world; 32 | size_t num_inliers; 33 | std::vector inlier_mask; 34 | 35 | if (!EstimateAbsolutePose(estimation_options, 36 | points2D, 37 | points3D, 38 | &cam_from_world, 39 | &camera, 40 | &num_inliers, 41 | &inlier_mask)) { 42 | return failure; 43 | } 44 | 45 | // Absolute pose refinement. 46 | Eigen::Matrix covariance; 47 | if (!RefineAbsolutePose(refinement_options, 48 | inlier_mask, 49 | points2D, 50 | points3D, 51 | &cam_from_world, 52 | &camera, 53 | return_covariance ? &covariance : nullptr)) { 54 | return failure; 55 | } 56 | 57 | py::gil_scoped_acquire acquire; 58 | py::dict success_dict("cam_from_world"_a = cam_from_world, 59 | "num_inliers"_a = num_inliers, 60 | "inliers"_a = ToPythonMask(inlier_mask)); 61 | if (return_covariance) success_dict["covariance"] = covariance; 62 | return success_dict; 63 | } 64 | 65 | py::object PyRefineAbsolutePose( 66 | const Rigid3d& init_cam_from_world, 67 | const std::vector& points2D, 68 | const std::vector& points3D, 69 | const PyInlierMask& inlier_mask, 70 | Camera& camera, 71 | const AbsolutePoseRefinementOptions& refinement_options) { 72 | SetPRNGSeed(0); 73 | THROW_CHECK_EQ(points2D.size(), points3D.size()); 74 | THROW_CHECK_EQ(inlier_mask.size(), points2D.size()); 75 | py::object failure = py::none(); 76 | py::gil_scoped_release release; 77 | 78 | Rigid3d refined_cam_from_world = init_cam_from_world; 79 | std::vector inlier_mask_char(inlier_mask.size()); 80 | Eigen::Map>( 81 | inlier_mask_char.data(), inlier_mask.size()) = inlier_mask.cast(); 82 | if (!RefineAbsolutePose(refinement_options, 83 | inlier_mask_char, 84 | points2D, 85 | points3D, 86 | &refined_cam_from_world, 87 | &camera)) { 88 | return failure; 89 | } 90 | 91 | // Success output dictionary. 92 | py::gil_scoped_acquire acquire; 93 | return py::dict("cam_from_world"_a = refined_cam_from_world); 94 | } 95 | 96 | void BindAbsolutePoseEstimator(py::module& m) { 97 | auto PyRANSACOptions = m.attr("RANSACOptions"); 98 | py::class_ PyEstimationOptions( 99 | m, "AbsolutePoseEstimationOptions"); 100 | PyEstimationOptions 101 | .def(py::init<>([PyRANSACOptions]() { 102 | AbsolutePoseEstimationOptions options; 103 | options.estimate_focal_length = false; 104 | // init through Python to obtain the new defaults defined in __init__ 105 | options.ransac_options = PyRANSACOptions().cast(); 106 | options.ransac_options.max_error = 12.0; 107 | return options; 108 | })) 109 | .def_readwrite("estimate_focal_length", 110 | &AbsolutePoseEstimationOptions::estimate_focal_length) 111 | .def_readwrite("num_focal_length_samples", 112 | &AbsolutePoseEstimationOptions::num_focal_length_samples) 113 | .def_readwrite("min_focal_length_ratio", 114 | &AbsolutePoseEstimationOptions::min_focal_length_ratio) 115 | .def_readwrite("max_focal_length_ratio", 116 | &AbsolutePoseEstimationOptions::max_focal_length_ratio) 117 | .def_readwrite("ransac", &AbsolutePoseEstimationOptions::ransac_options); 118 | MakeDataclass(PyEstimationOptions); 119 | auto est_options = 120 | PyEstimationOptions().cast(); 121 | 122 | py::class_ PyRefinementOptions( 123 | m, "AbsolutePoseRefinementOptions"); 124 | PyRefinementOptions 125 | .def(py::init<>([]() { 126 | AbsolutePoseRefinementOptions options; 127 | options.refine_focal_length = false; 128 | options.refine_extra_params = false; 129 | options.print_summary = false; 130 | return options; 131 | })) 132 | .def_readwrite("gradient_tolerance", 133 | &AbsolutePoseRefinementOptions::gradient_tolerance) 134 | .def_readwrite("max_num_iterations", 135 | &AbsolutePoseRefinementOptions::max_num_iterations) 136 | .def_readwrite("loss_function_scale", 137 | &AbsolutePoseRefinementOptions::loss_function_scale) 138 | .def_readwrite("refine_focal_length", 139 | &AbsolutePoseRefinementOptions::refine_focal_length) 140 | .def_readwrite("refine_extra_params", 141 | &AbsolutePoseRefinementOptions::refine_extra_params) 142 | .def_readwrite("print_summary", 143 | &AbsolutePoseRefinementOptions::print_summary); 144 | MakeDataclass(PyRefinementOptions); 145 | auto ref_options = 146 | PyRefinementOptions().cast(); 147 | 148 | m.def("absolute_pose_estimation", 149 | &PyEstimateAndRefineAbsolutePose, 150 | "points2D"_a, 151 | "points3D"_a, 152 | "camera"_a, 153 | "estimation_options"_a = est_options, 154 | "refinement_options"_a = ref_options, 155 | "return_covariance"_a = false, 156 | "Absolute pose estimation with non-linear refinement."); 157 | 158 | m.def("pose_refinement", 159 | &PyRefineAbsolutePose, 160 | "cam_from_world"_a, 161 | "points2D"_a, 162 | "points3D"_a, 163 | "inlier_mask"_a, 164 | "camera"_a, 165 | "refinement_options"_a = ref_options, 166 | "Non-linear refinement of absolute pose."); 167 | } 168 | -------------------------------------------------------------------------------- /pycolmap/estimators/alignment.h: -------------------------------------------------------------------------------- 1 | #include "colmap/estimators/alignment.h" 2 | #include "colmap/exe/model.h" 3 | #include "colmap/geometry/sim3.h" 4 | #include "colmap/scene/reconstruction.h" 5 | 6 | #include "pycolmap/log_exceptions.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace colmap; 14 | using namespace pybind11::literals; 15 | namespace py = pybind11; 16 | 17 | void BindAlignmentEstimator(py::module& m) { 18 | py::class_(m, "ImageAlignmentError") 19 | .def(py::init<>()) 20 | .def_readwrite("image_name", &ImageAlignmentError::image_name) 21 | .def_readwrite("rotation_error_deg", 22 | &ImageAlignmentError::rotation_error_deg) 23 | .def_readwrite("proj_center_error", 24 | &ImageAlignmentError::proj_center_error); 25 | 26 | m.def( 27 | "align_reconstructions_via_reprojections", 28 | [](const Reconstruction& src_reconstruction, 29 | const Reconstruction& tgt_reconstruction, 30 | const double min_inlier_observations, 31 | const double max_reproj_error) { 32 | THROW_CHECK_GE(min_inlier_observations, 0.0); 33 | THROW_CHECK_LE(min_inlier_observations, 1.0); 34 | Sim3d tgt_from_src; 35 | THROW_CHECK( 36 | AlignReconstructionsViaReprojections(src_reconstruction, 37 | tgt_reconstruction, 38 | min_inlier_observations, 39 | max_reproj_error, 40 | &tgt_from_src)); 41 | return tgt_from_src; 42 | }, 43 | "src_reconstruction"_a, 44 | "tgt_reconstruction"_a, 45 | "min_inlier_observations"_a = 0.3, 46 | "max_reproj_error"_a = 8.0); 47 | 48 | m.def( 49 | "align_reconstructions_via_proj_centers", 50 | [](const Reconstruction& src_reconstruction, 51 | const Reconstruction& tgt_reconstruction, 52 | const double max_proj_center_error) { 53 | THROW_CHECK_GT(max_proj_center_error, 0.0); 54 | Sim3d tgt_from_src; 55 | THROW_CHECK(AlignReconstructionsViaProjCenters(src_reconstruction, 56 | tgt_reconstruction, 57 | max_proj_center_error, 58 | &tgt_from_src)); 59 | return tgt_from_src; 60 | }, 61 | "src_reconstruction"_a, 62 | "tgt_reconstruction"_a, 63 | "max_proj_center_error"_a); 64 | 65 | m.def( 66 | "align_reconstructions_via_points", 67 | [](const Reconstruction& src_reconstruction, 68 | const Reconstruction& tgt_reconstruction, 69 | const size_t min_common_observations, 70 | const double max_error, 71 | const double min_inlier_ratio) { 72 | THROW_CHECK_GT(min_common_observations, 0); 73 | THROW_CHECK_GT(max_error, 0.0); 74 | THROW_CHECK_GE(min_inlier_ratio, 0.0); 75 | THROW_CHECK_LE(min_inlier_ratio, 1.0); 76 | Sim3d tgt_from_src; 77 | THROW_CHECK(AlignReconstructionsViaPoints(src_reconstruction, 78 | tgt_reconstruction, 79 | min_common_observations, 80 | max_error, 81 | min_inlier_ratio, 82 | &tgt_from_src)); 83 | return tgt_from_src; 84 | }, 85 | "src_reconstruction"_a, 86 | "tgt_reconstruction"_a, 87 | "min_common_observations"_a = 3, 88 | "max_error"_a = 0.005, 89 | "min_inlier_ratio"_a = 0.9); 90 | 91 | m.def( 92 | "align_reconstrution_to_locations", 93 | [](const Reconstruction& src, 94 | const std::vector& image_names, 95 | const std::vector& locations, 96 | const int min_common_images, 97 | const RANSACOptions& ransac_options) { 98 | THROW_CHECK_GE(min_common_images, 3); 99 | THROW_CHECK_EQ(image_names.size(), locations.size()); 100 | Sim3d locationsFromSrc; 101 | THROW_CHECK(AlignReconstructionToLocations(src, 102 | image_names, 103 | locations, 104 | min_common_images, 105 | ransac_options, 106 | &locationsFromSrc)); 107 | return locationsFromSrc; 108 | }, 109 | "src"_a, 110 | "image_names"_a, 111 | "locations"_a, 112 | "min_common_points"_a, 113 | "ransac_options"_a); 114 | 115 | m.def( 116 | "compare_reconstructions", 117 | [](const Reconstruction& reconstruction1, 118 | const Reconstruction& reconstruction2, 119 | const std::string& alignment_error, 120 | double min_inlier_observations, 121 | double max_reproj_error, 122 | double max_proj_center_error) { 123 | std::vector errors; 124 | Sim3d rec2_from_rec1; 125 | THROW_CUSTOM_CHECK_MSG(CompareModels(reconstruction1, 126 | reconstruction2, 127 | alignment_error, 128 | min_inlier_observations, 129 | max_reproj_error, 130 | max_proj_center_error, 131 | errors, 132 | rec2_from_rec1), 133 | std::runtime_error, 134 | "=> Reconstruction alignment failed."); 135 | return py::dict("rec2_from_rec1"_a = rec2_from_rec1, 136 | "errors"_a = errors); 137 | }, 138 | "reconstruction1"_a, 139 | "reconstruction2"_a, 140 | "alignment_error"_a = "reprojection", 141 | "min_inlier_observations"_a = 0.3, 142 | "max_reproj_error"_a = 8.0, 143 | "max_proj_center_error"_a = 0.1); 144 | } 145 | -------------------------------------------------------------------------------- /pycolmap/estimators/bindings.h: -------------------------------------------------------------------------------- 1 | #include "pycolmap/estimators/absolute_pose.h" 2 | #include "pycolmap/estimators/alignment.h" 3 | #include "pycolmap/estimators/essential_matrix.h" 4 | #include "pycolmap/estimators/fundamental_matrix.h" 5 | #include "pycolmap/estimators/generalized_absolute_pose.h" 6 | #include "pycolmap/estimators/homography_matrix.h" 7 | #include "pycolmap/estimators/triangulation.h" 8 | #include "pycolmap/estimators/two_view_geometry.h" 9 | 10 | #include 11 | 12 | namespace py = pybind11; 13 | 14 | void BindEstimators(py::module& m) { 15 | BindAbsolutePoseEstimator(m); 16 | BindAlignmentEstimator(m); 17 | BindEssentialMatrixEstimator(m); 18 | BindFundamentalMatrixEstimator(m); 19 | BindGeneralizedAbsolutePoseEstimator(m); 20 | BindHomographyMatrixEstimator(m); 21 | BindTriangulationEstimator(m); 22 | BindTwoViewGeometryEstimator(m); 23 | } 24 | -------------------------------------------------------------------------------- /pycolmap/estimators/essential_matrix.h: -------------------------------------------------------------------------------- 1 | #include "colmap/estimators/essential_matrix.h" 2 | #include "colmap/geometry/essential_matrix.h" 3 | #include "colmap/geometry/rigid3.h" 4 | #include "colmap/math/random.h" 5 | #include "colmap/optim/loransac.h" 6 | #include "colmap/scene/camera.h" 7 | 8 | #include "pycolmap/log_exceptions.h" 9 | #include "pycolmap/utils.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | using namespace colmap; 16 | using namespace pybind11::literals; 17 | namespace py = pybind11; 18 | 19 | py::object PyEstimateAndDecomposeEssentialMatrix( 20 | const std::vector& points2D1, 21 | const std::vector& points2D2, 22 | Camera& camera1, 23 | Camera& camera2, 24 | const RANSACOptions& options) { 25 | SetPRNGSeed(0); 26 | THROW_CHECK_EQ(points2D1.size(), points2D2.size()); 27 | py::object failure = py::none(); 28 | py::gil_scoped_release release; 29 | 30 | // Image to world. 31 | std::vector world_points2D1; 32 | for (size_t idx = 0; idx < points2D1.size(); ++idx) { 33 | world_points2D1.push_back(camera1.CamFromImg(points2D1[idx])); 34 | } 35 | 36 | std::vector world_points2D2; 37 | for (size_t idx = 0; idx < points2D2.size(); ++idx) { 38 | world_points2D2.push_back(camera2.CamFromImg(points2D2[idx])); 39 | } 40 | 41 | // Compute world error. 42 | const double max_error_px = options.max_error; 43 | const double max_error = 0.5 * (max_error_px / camera1.MeanFocalLength() + 44 | max_error_px / camera2.MeanFocalLength()); 45 | RANSACOptions ransac_options(options); 46 | ransac_options.max_error = max_error; 47 | 48 | LORANSAC 49 | ransac(ransac_options); 50 | 51 | // Essential matrix estimation. 52 | const auto report = ransac.Estimate(world_points2D1, world_points2D2); 53 | 54 | if (!report.success) { 55 | return failure; 56 | } 57 | 58 | // Recover data from report. 59 | const Eigen::Matrix3d E = report.model; 60 | const size_t num_inliers = report.support.num_inliers; 61 | const auto& inlier_mask = report.inlier_mask; 62 | 63 | // Pose from essential matrix. 64 | std::vector inlier_world_points2D1; 65 | std::vector inlier_world_points2D2; 66 | 67 | for (size_t idx = 0; idx < inlier_mask.size(); ++idx) { 68 | if (inlier_mask[idx]) { 69 | inlier_world_points2D1.push_back(world_points2D1[idx]); 70 | inlier_world_points2D2.push_back(world_points2D2[idx]); 71 | } 72 | } 73 | 74 | Rigid3d cam2_from_cam1; 75 | Eigen::Matrix3d cam2_from_cam1_rot_mat; 76 | std::vector points3D; 77 | PoseFromEssentialMatrix(E, 78 | inlier_world_points2D1, 79 | inlier_world_points2D2, 80 | &cam2_from_cam1_rot_mat, 81 | &cam2_from_cam1.translation, 82 | &points3D); 83 | cam2_from_cam1.rotation = Eigen::Quaterniond(cam2_from_cam1_rot_mat); 84 | 85 | py::gil_scoped_acquire acquire; 86 | return py::dict("E"_a = E, 87 | "cam2_from_cam1"_a = cam2_from_cam1, 88 | "num_inliers"_a = num_inliers, 89 | "inliers"_a = ToPythonMask(inlier_mask)); 90 | } 91 | 92 | void BindEssentialMatrixEstimator(py::module& m) { 93 | auto est_options = m.attr("RANSACOptions")().cast(); 94 | 95 | m.def("essential_matrix_estimation", 96 | &PyEstimateAndDecomposeEssentialMatrix, 97 | "points2D1"_a, 98 | "points2D2"_a, 99 | "camera1"_a, 100 | "camera2"_a, 101 | "estimation_options"_a = est_options, 102 | "LORANSAC + 5-point algorithm."); 103 | } 104 | -------------------------------------------------------------------------------- /pycolmap/estimators/fundamental_matrix.h: -------------------------------------------------------------------------------- 1 | #include "colmap/estimators/fundamental_matrix.h" 2 | #include "colmap/math/random.h" 3 | #include "colmap/optim/loransac.h" 4 | #include "colmap/scene/camera.h" 5 | 6 | #include "pycolmap/log_exceptions.h" 7 | #include "pycolmap/utils.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace colmap; 14 | using namespace pybind11::literals; 15 | namespace py = pybind11; 16 | 17 | py::object PyEstimateFundamentalMatrix( 18 | const std::vector& points2D1, 19 | const std::vector& points2D2, 20 | const RANSACOptions& options) { 21 | SetPRNGSeed(0); 22 | THROW_CHECK_EQ(points2D1.size(), points2D2.size()); 23 | py::object failure = py::none(); 24 | py::gil_scoped_release release; 25 | 26 | LORANSAC 28 | ransac(options); 29 | const auto report = ransac.Estimate(points2D1, points2D2); 30 | if (!report.success) { 31 | return failure; 32 | } 33 | 34 | const Eigen::Matrix3d F = report.model; 35 | py::gil_scoped_acquire acquire; 36 | return py::dict("F"_a = F, 37 | "num_inliers"_a = report.support.num_inliers, 38 | "inliers"_a = ToPythonMask(report.inlier_mask)); 39 | } 40 | 41 | void BindFundamentalMatrixEstimator(py::module& m) { 42 | auto est_options = m.attr("RANSACOptions")().cast(); 43 | 44 | m.def("fundamental_matrix_estimation", 45 | &PyEstimateFundamentalMatrix, 46 | "points2D1"_a, 47 | "points2D2"_a, 48 | "estimation_options"_a = est_options, 49 | "LORANSAC + 7-point algorithm."); 50 | } 51 | -------------------------------------------------------------------------------- /pycolmap/estimators/generalized_absolute_pose.h: -------------------------------------------------------------------------------- 1 | #include "colmap/estimators/generalized_pose.h" 2 | #include "colmap/estimators/pose.h" 3 | #include "colmap/geometry/rigid3.h" 4 | #include "colmap/math/random.h" 5 | #include "colmap/optim/ransac.h" 6 | #include "colmap/scene/camera.h" 7 | 8 | #include "pycolmap/log_exceptions.h" 9 | #include "pycolmap/utils.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | using namespace colmap; 16 | using namespace pybind11::literals; 17 | namespace py = pybind11; 18 | 19 | py::object PyEstimateAndRefineGeneralizedAbsolutePose( 20 | const std::vector& points2D, 21 | const std::vector& points3D, 22 | const std::vector& camera_idxs, 23 | const std::vector& cams_from_rig, 24 | std::vector& cameras, 25 | const RANSACOptions& ransac_options, 26 | const AbsolutePoseRefinementOptions& refinement_options, 27 | const bool return_covariance) { 28 | SetPRNGSeed(0); 29 | THROW_CHECK_EQ(points2D.size(), points3D.size()); 30 | THROW_CHECK_EQ(points2D.size(), camera_idxs.size()); 31 | THROW_CHECK_EQ(cams_from_rig.size(), cameras.size()); 32 | THROW_CHECK_GE(*std::min_element(camera_idxs.begin(), camera_idxs.end()), 0); 33 | THROW_CHECK_LT(*std::max_element(camera_idxs.begin(), camera_idxs.end()), 34 | cameras.size()); 35 | 36 | py::object failure = py::none(); 37 | py::gil_scoped_release release; 38 | 39 | Rigid3d rig_from_world; 40 | size_t num_inliers; 41 | std::vector inlier_mask; 42 | if (!EstimateGeneralizedAbsolutePose(ransac_options, 43 | points2D, 44 | points3D, 45 | camera_idxs, 46 | cams_from_rig, 47 | cameras, 48 | &rig_from_world, 49 | &num_inliers, 50 | &inlier_mask)) { 51 | return failure; 52 | } 53 | 54 | // Absolute pose refinement. 55 | Eigen::Matrix covariance; 56 | if (!RefineGeneralizedAbsolutePose( 57 | refinement_options, 58 | inlier_mask, 59 | points2D, 60 | points3D, 61 | camera_idxs, 62 | cams_from_rig, 63 | &rig_from_world, 64 | &cameras, 65 | return_covariance ? &covariance : nullptr)) { 66 | return failure; 67 | } 68 | 69 | py::gil_scoped_acquire acquire; 70 | py::dict success_dict("rig_from_world"_a = rig_from_world, 71 | "num_inliers"_a = num_inliers, 72 | "inliers"_a = ToPythonMask(inlier_mask)); 73 | if (return_covariance) success_dict["covariance"] = covariance; 74 | return success_dict; 75 | } 76 | 77 | void BindGeneralizedAbsolutePoseEstimator(py::module& m) { 78 | auto est_options = m.attr("RANSACOptions")().cast(); 79 | auto ref_options = m.attr("AbsolutePoseRefinementOptions")() 80 | .cast(); 81 | 82 | m.def( 83 | "rig_absolute_pose_estimation", 84 | &PyEstimateAndRefineGeneralizedAbsolutePose, 85 | "points2D"_a, 86 | "points3D"_a, 87 | "cameras"_a, 88 | "camera_idxs"_a, 89 | "cams_from_rig"_a, 90 | "estimation_options"_a = est_options, 91 | "refinement_options"_a = ref_options, 92 | "return_covariance"_a = false, 93 | "Absolute pose estimation with non-linear refinement for a multi-camera " 94 | "rig."); 95 | } 96 | -------------------------------------------------------------------------------- /pycolmap/estimators/homography_matrix.h: -------------------------------------------------------------------------------- 1 | #include "colmap/estimators/homography_matrix.h" 2 | #include "colmap/math/random.h" 3 | #include "colmap/optim/loransac.h" 4 | 5 | #include "pycolmap/log_exceptions.h" 6 | #include "pycolmap/utils.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace colmap; 13 | using namespace pybind11::literals; 14 | namespace py = pybind11; 15 | 16 | py::object PyEstimateHomographyMatrix( 17 | const std::vector& points2D1, 18 | const std::vector& points2D2, 19 | const RANSACOptions& options) { 20 | SetPRNGSeed(0); 21 | THROW_CHECK_EQ(points2D1.size(), points2D2.size()); 22 | py::object failure = py::none(); 23 | py::gil_scoped_release release; 24 | 25 | LORANSAC H_ransac( 26 | options); 27 | const auto report = H_ransac.Estimate(points2D1, points2D2); 28 | if (!report.success) { 29 | return failure; 30 | } 31 | 32 | const Eigen::Matrix3d H = report.model; 33 | py::gil_scoped_acquire acquire; 34 | return py::dict("H"_a = H, 35 | "num_inliers"_a = report.support.num_inliers, 36 | "inliers"_a = ToPythonMask(report.inlier_mask)); 37 | } 38 | 39 | void BindHomographyMatrixEstimator(py::module& m) { 40 | auto est_options = m.attr("RANSACOptions")().cast(); 41 | 42 | m.def("homography_matrix_estimation", 43 | &PyEstimateHomographyMatrix, 44 | "points2D1"_a, 45 | "points2D2"_a, 46 | "estimation_options"_a = est_options, 47 | "LORANSAC + 4-point DLT algorithm."); 48 | } 49 | -------------------------------------------------------------------------------- /pycolmap/estimators/triangulation.h: -------------------------------------------------------------------------------- 1 | #include "colmap/estimators/triangulation.h" 2 | #include "colmap/scene/camera.h" 3 | #include "colmap/scene/image.h" 4 | 5 | #include "pycolmap/helpers.h" 6 | #include "pycolmap/log_exceptions.h" 7 | #include "pycolmap/utils.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace colmap; 14 | using namespace pybind11::literals; 15 | namespace py = pybind11; 16 | 17 | py::dict PyEstimateTriangulation( 18 | const std::vector& point_data, 19 | const std::vector& images, 20 | const std::vector& cameras, 21 | const EstimateTriangulationOptions& options) { 22 | SetPRNGSeed(0); 23 | THROW_CHECK_EQ(images.size(), cameras.size()); 24 | THROW_CHECK_EQ(images.size(), point_data.size()); 25 | py::object failure = py::none(); 26 | py::gil_scoped_release release; 27 | 28 | std::vector pose_data; 29 | pose_data.reserve(images.size()); 30 | for (size_t i = 0; i < images.size(); ++i) { 31 | pose_data.emplace_back(images[i].CamFromWorld().ToMatrix(), 32 | images[i].ProjectionCenter(), 33 | &cameras[i]); 34 | } 35 | Eigen::Vector3d xyz; 36 | std::vector inlier_mask; 37 | if (!EstimateTriangulation( 38 | options, point_data, pose_data, &inlier_mask, &xyz)) { 39 | return failure; 40 | } 41 | 42 | py::gil_scoped_acquire acquire; 43 | return py::dict("xyz"_a = xyz, "inliers"_a = ToPythonMask(inlier_mask)); 44 | } 45 | 46 | void BindTriangulationEstimator(py::module& m) { 47 | auto PyRANSACOptions = m.attr("RANSACOptions"); 48 | 49 | py::class_(m, "PointData") 50 | .def(py::init()); 51 | 52 | py::class_ PyTriangulationOptions( 53 | m, "EstimateTriangulationOptions"); 54 | PyTriangulationOptions 55 | .def(py::init<>([PyRANSACOptions]() { 56 | EstimateTriangulationOptions options; 57 | // init through Python to obtain the new defaults defined in __init__ 58 | options.ransac_options = PyRANSACOptions().cast(); 59 | return options; 60 | })) 61 | .def_readwrite("min_tri_angle", 62 | &EstimateTriangulationOptions::min_tri_angle) 63 | .def_readwrite("ransac", &EstimateTriangulationOptions::ransac_options); 64 | MakeDataclass(PyTriangulationOptions); 65 | auto triangulation_options = 66 | PyTriangulationOptions().cast(); 67 | 68 | m.def("estimate_triangulation", 69 | &PyEstimateTriangulation, 70 | "point_data"_a, 71 | "images"_a, 72 | "cameras"_a, 73 | "opions"_a = triangulation_options, 74 | "Robustly estimate 3D point from observations in multiple views using " 75 | "RANSAC"); 76 | } 77 | -------------------------------------------------------------------------------- /pycolmap/estimators/two_view_geometry.h: -------------------------------------------------------------------------------- 1 | #include "colmap/estimators/two_view_geometry.h" 2 | #include "colmap/estimators/utils.h" 3 | #include "colmap/scene/camera.h" 4 | #include "colmap/scene/two_view_geometry.h" 5 | 6 | #include "pycolmap/helpers.h" 7 | #include "pycolmap/log_exceptions.h" 8 | #include "pycolmap/utils.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | using namespace colmap; 15 | using namespace pybind11::literals; 16 | namespace py = pybind11; 17 | 18 | // TODO(sarlinpe): Consider changing the COLMAP type. 19 | typedef Eigen::Matrix 20 | PyFeatureMatches; 21 | 22 | PyFeatureMatches FeatureMatchesToMatrix(const FeatureMatches& matches) { 23 | PyFeatureMatches matrix(matches.size(), 2); 24 | for (size_t i = 0; i < matches.size(); i++) { 25 | matrix(i, 0) = matches[i].point2D_idx1; 26 | matrix(i, 1) = matches[i].point2D_idx2; 27 | } 28 | return matrix; 29 | } 30 | 31 | FeatureMatches FeatureMatchesFromMatrix(const PyFeatureMatches& matrix) { 32 | FeatureMatches matches(matrix.rows()); 33 | for (size_t i = 0; i < matches.size(); i++) { 34 | matches[i].point2D_idx1 = matrix(i, 0); 35 | matches[i].point2D_idx2 = matrix(i, 1); 36 | } 37 | return matches; 38 | } 39 | 40 | void BindTwoViewGeometryEstimator(py::module& m) { 41 | py::class_ PyTwoViewGeometryOptions( 42 | m, "TwoViewGeometryOptions"); 43 | PyTwoViewGeometryOptions.def(py::init<>()) 44 | .def_readwrite("min_num_inliers", 45 | &TwoViewGeometryOptions::min_num_inliers) 46 | .def_readwrite("min_E_F_inlier_ratio", 47 | &TwoViewGeometryOptions::min_E_F_inlier_ratio) 48 | .def_readwrite("max_H_inlier_ratio", 49 | &TwoViewGeometryOptions::max_H_inlier_ratio) 50 | .def_readwrite("watermark_min_inlier_ratio", 51 | &TwoViewGeometryOptions::watermark_min_inlier_ratio) 52 | .def_readwrite("watermark_border_size", 53 | &TwoViewGeometryOptions::watermark_border_size) 54 | .def_readwrite("detect_watermark", 55 | &TwoViewGeometryOptions::detect_watermark) 56 | .def_readwrite("multiple_ignore_watermark", 57 | &TwoViewGeometryOptions::multiple_ignore_watermark) 58 | .def_readwrite("force_H_use", &TwoViewGeometryOptions::force_H_use) 59 | .def_readwrite("compute_relative_pose", 60 | &TwoViewGeometryOptions::compute_relative_pose) 61 | .def_readwrite("multiple_models", 62 | &TwoViewGeometryOptions::multiple_models) 63 | .def_readwrite("ransac", &TwoViewGeometryOptions::ransac_options); 64 | MakeDataclass(PyTwoViewGeometryOptions); 65 | auto tvg_options = PyTwoViewGeometryOptions().cast(); 66 | 67 | py::enum_(m, 68 | "TwoViewGeometryConfiguration") 69 | .value("UNDEFINED", TwoViewGeometry::UNDEFINED) 70 | .value("DEGENERATE", TwoViewGeometry::DEGENERATE) 71 | .value("CALIBRATED", TwoViewGeometry::CALIBRATED) 72 | .value("UNCALIBRATED", TwoViewGeometry::UNCALIBRATED) 73 | .value("PLANAR", TwoViewGeometry::PLANAR) 74 | .value("PANORAMIC", TwoViewGeometry::PANORAMIC) 75 | .value("PLANAR_OR_PANORAMIC", TwoViewGeometry::PLANAR_OR_PANORAMIC) 76 | .value("WATERMARK", TwoViewGeometry::WATERMARK) 77 | .value("MULTIPLE", TwoViewGeometry::MULTIPLE); 78 | 79 | py::class_ PyTwoViewGeometry(m, "TwoViewGeometry"); 80 | PyTwoViewGeometry.def(py::init<>()) 81 | .def_readonly("config", &TwoViewGeometry::config) 82 | .def_readonly("E", &TwoViewGeometry::E) 83 | .def_readonly("F", &TwoViewGeometry::F) 84 | .def_readonly("H", &TwoViewGeometry::H) 85 | .def_readonly("cam2_from_cam1", &TwoViewGeometry::cam2_from_cam1) 86 | .def_property_readonly( 87 | "inlier_matches", 88 | [](const TwoViewGeometry& self) { 89 | return FeatureMatchesToMatrix(self.inlier_matches); 90 | }) 91 | .def_readonly("tri_angle", &TwoViewGeometry::tri_angle) 92 | .def("invert", &TwoViewGeometry::Invert); 93 | MakeDataclass(PyTwoViewGeometry); 94 | 95 | m.def( 96 | "estimate_calibrated_two_view_geometry", 97 | [](const Camera& camera1, 98 | const std::vector& points1, 99 | const Camera& camera2, 100 | const std::vector& points2, 101 | const PyFeatureMatches* matches_ptr, 102 | const TwoViewGeometryOptions& options) { 103 | py::gil_scoped_release release; 104 | FeatureMatches matches; 105 | if (matches_ptr != nullptr) { 106 | matches = FeatureMatchesFromMatrix(*matches_ptr); 107 | } else { 108 | THROW_CHECK_EQ(points1.size(), points2.size()); 109 | matches.reserve(points1.size()); 110 | for (size_t i = 0; i < points1.size(); i++) { 111 | matches.emplace_back(i, i); 112 | } 113 | } 114 | return EstimateCalibratedTwoViewGeometry( 115 | camera1, points1, camera2, points2, matches, options); 116 | }, 117 | "camera1"_a, 118 | "points1"_a, 119 | "camera2"_a, 120 | "points2"_a, 121 | "matches"_a = py::none(), 122 | "options"_a = tvg_options); 123 | 124 | m.def( 125 | "estimate_two_view_geometry", 126 | [](const Camera& camera1, 127 | const std::vector& points1, 128 | const Camera& camera2, 129 | const std::vector& points2, 130 | const PyFeatureMatches* matches_ptr, 131 | const TwoViewGeometryOptions& options) { 132 | py::gil_scoped_release release; 133 | FeatureMatches matches; 134 | if (matches_ptr != nullptr) { 135 | matches = FeatureMatchesFromMatrix(*matches_ptr); 136 | } else { 137 | THROW_CHECK_EQ(points1.size(), points2.size()); 138 | matches.reserve(points1.size()); 139 | for (size_t i = 0; i < points1.size(); i++) { 140 | matches.emplace_back(i, i); 141 | } 142 | } 143 | return EstimateTwoViewGeometry( 144 | camera1, points1, camera2, points2, matches, options); 145 | }, 146 | "camera1"_a, 147 | "points1"_a, 148 | "camera2"_a, 149 | "points2"_a, 150 | "matches"_a = py::none(), 151 | "options"_a = tvg_options); 152 | 153 | m.def("estimate_two_view_geometry_pose", 154 | &EstimateTwoViewGeometryPose, 155 | "camera1"_a, 156 | "points1"_a, 157 | "camera2"_a, 158 | "points2"_a, 159 | "geometry"_a); 160 | 161 | m.def( 162 | "squared_sampson_error", 163 | [](const std::vector& points1, 164 | const std::vector& points2, 165 | const Eigen::Matrix3d& E) { 166 | std::vector residuals; 167 | ComputeSquaredSampsonError(points1, points2, E, &residuals); 168 | return residuals; 169 | }, 170 | "points2D1"_a, 171 | "points2D2"_a, 172 | "E"_a, 173 | "Calculate the squared Sampson error for a given essential or " 174 | "fundamental matrix.", 175 | py::call_guard()); 176 | } 177 | -------------------------------------------------------------------------------- /pycolmap/feature/sift.h: -------------------------------------------------------------------------------- 1 | #include "colmap/feature/sift.h" 2 | #include "colmap/feature/utils.h" 3 | 4 | #include "pycolmap/helpers.h" 5 | #include "pycolmap/utils.h" 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define kdim 4 15 | 16 | using namespace colmap; 17 | using namespace pybind11::literals; 18 | namespace py = pybind11; 19 | 20 | template 21 | using pyimage_t = 22 | Eigen::Matrix; 23 | 24 | typedef Eigen::Matrix 25 | descriptors_t; 26 | typedef Eigen::Matrix keypoints_t; 27 | typedef std::tuple sift_output_t; 28 | 29 | static std::map> sift_gpu_mutexes; 30 | 31 | class Sift { 32 | public: 33 | Sift(SiftExtractionOptions options, Device device) 34 | : options_(std::move(options)), use_gpu_(IsGPU(device)) { 35 | VerifyGPUParams(use_gpu_); 36 | options_.use_gpu = use_gpu_; 37 | extractor_ = CreateSiftFeatureExtractor(options_); 38 | THROW_CHECK(extractor_ != nullptr); 39 | } 40 | 41 | sift_output_t Extract(const Eigen::Ref>& image) { 42 | THROW_CHECK_LE(image.rows(), options_.max_image_size); 43 | THROW_CHECK_LE(image.cols(), options_.max_image_size); 44 | 45 | const unsigned int bpp = 8; // Grey. 46 | const unsigned int width = image.cols(); 47 | const unsigned int scan_width = (bpp / 8) * width; 48 | pyimage_t image_copy = image; 49 | FIBITMAP* bitmap_raw = FreeImage_ConvertFromRawBitsEx( 50 | /*copySource=*/false, 51 | static_cast(image_copy.data()), 52 | FIT_BITMAP, 53 | width, 54 | image.rows(), 55 | scan_width, 56 | bpp, 57 | FI_RGBA_RED_MASK, 58 | FI_RGBA_GREEN_MASK, 59 | FI_RGBA_BLUE_MASK, 60 | /*topdown=*/true); 61 | const Bitmap bitmap(bitmap_raw); 62 | 63 | FeatureKeypoints keypoints_; 64 | FeatureDescriptors descriptors_; 65 | THROW_CHECK(extractor_->Extract(bitmap, &keypoints_, &descriptors_)) 66 | const size_t num_features = keypoints_.size(); 67 | 68 | keypoints_t keypoints(num_features, kdim); 69 | for (size_t i = 0; i < num_features; ++i) { 70 | keypoints(i, 0) = keypoints_[i].x; 71 | keypoints(i, 1) = keypoints_[i].y; 72 | keypoints(i, 2) = keypoints_[i].ComputeScale(); 73 | keypoints(i, 3) = keypoints_[i].ComputeOrientation(); 74 | } 75 | 76 | descriptors_t descriptors = descriptors_.cast(); 77 | descriptors /= 512.0f; 78 | 79 | return std::make_tuple(keypoints, descriptors); 80 | } 81 | 82 | sift_output_t Extract(const Eigen::Ref>& image) { 83 | const pyimage_t image_f = (image * 255.0f).cast(); 84 | return Extract(image_f); 85 | } 86 | 87 | const SiftExtractionOptions& Options() const { return options_; }; 88 | 89 | Device GetDevice() const { return (use_gpu_) ? Device::CUDA : Device::CPU; }; 90 | 91 | private: 92 | std::unique_ptr extractor_; 93 | SiftExtractionOptions options_; 94 | bool use_gpu_ = false; 95 | }; 96 | 97 | void BindSift(py::module& m) { 98 | // For backwards consistency 99 | py::dict sift_options; 100 | sift_options["peak_threshold"] = 0.01; 101 | sift_options["first_octave"] = 0; 102 | sift_options["max_image_size"] = 7000; 103 | 104 | py::class_(m, "Sift") 105 | .def(py::init(), 106 | "options"_a = sift_options, 107 | "device"_a = Device::AUTO) 108 | .def("extract", 109 | py::overload_cast>&>( 110 | &Sift::Extract), 111 | "image"_a.noconvert()) 112 | .def("extract", 113 | py::overload_cast>&>( 114 | &Sift::Extract), 115 | "image"_a.noconvert()) 116 | .def_property_readonly("options", &Sift::Options) 117 | .def_property_readonly("device", &Sift::GetDevice); 118 | } 119 | -------------------------------------------------------------------------------- /pycolmap/geometry/bindings.h: -------------------------------------------------------------------------------- 1 | #include "colmap/geometry/essential_matrix.h" 2 | #include "colmap/geometry/pose.h" 3 | #include "colmap/geometry/rigid3.h" 4 | #include "colmap/geometry/sim3.h" 5 | 6 | #include "pycolmap/geometry/homography_matrix.h" 7 | #include "pycolmap/helpers.h" 8 | #include "pycolmap/pybind11_extension.h" 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace py = pybind11; 19 | using namespace pybind11::literals; 20 | 21 | void BindGeometry(py::module& m) { 22 | BindHomographyGeometry(m); 23 | 24 | py::class_ext_ PyRotation3d(m, "Rotation3d"); 25 | PyRotation3d.def(py::init([]() { return Eigen::Quaterniond::Identity(); })) 26 | .def(py::init(), 27 | "xyzw"_a, 28 | "Quaternion in [x,y,z,w] format.") 29 | .def(py::init(), 30 | "rotmat"_a, 31 | "3x3 rotation matrix.") 32 | .def(py::init([](const Eigen::Vector3d& vec) { 33 | return Eigen::Quaterniond( 34 | Eigen::AngleAxis(vec.norm(), vec.normalized())); 35 | }), 36 | "axis_angle"_a, 37 | "Axis-angle 3D vector.") 38 | .def_property( 39 | "quat", 40 | py::overload_cast<>(&Eigen::Quaterniond::coeffs), 41 | [](Eigen::Quaterniond& self, const Eigen::Vector4d& quat) { 42 | self.coeffs() = quat; 43 | }, 44 | "Quaternion in [x,y,z,w] format.") 45 | .def(py::self * Eigen::Quaterniond()) 46 | .def(py::self * Eigen::Vector3d()) 47 | .def("__mul__", 48 | [](const Eigen::Quaterniond& self, 49 | const py::EigenDRef& points) 50 | -> Eigen::MatrixX3d { 51 | return points * self.toRotationMatrix().transpose(); 52 | }) 53 | .def("normalize", &Eigen::Quaterniond::normalize) 54 | .def("matrix", &Eigen::Quaterniond::toRotationMatrix) 55 | .def("norm", &Eigen::Quaterniond::norm) 56 | .def("angle", 57 | [](const Eigen::Quaterniond& self) { 58 | return Eigen::AngleAxis(self).angle(); 59 | }) 60 | .def("angle_to", 61 | [](const Eigen::Quaterniond& self, const Eigen::Quaterniond& other) { 62 | return self.angularDistance(other); 63 | }) 64 | .def("inverse", &Eigen::Quaterniond::inverse) 65 | .def("__repr__", [](const Eigen::Quaterniond& self) { 66 | std::stringstream ss; 67 | ss << "Rotation3d(quat_xyzw=[" << self.coeffs().format(vec_fmt) << "])"; 68 | return ss.str(); 69 | }); 70 | py::implicitly_convertible(); 71 | MakeDataclass(PyRotation3d); 72 | 73 | py::class_ext_ PyRigid3d(m, "Rigid3d"); 74 | PyRigid3d.def(py::init<>()) 75 | .def(py::init()) 76 | .def(py::init([](const Eigen::Matrix3x4d& matrix) { 77 | return Rigid3d(Eigen::Quaterniond(matrix.leftCols<3>()), matrix.col(3)); 78 | })) 79 | .def_readwrite("rotation", &Rigid3d::rotation) 80 | .def_readwrite("translation", &Rigid3d::translation) 81 | .def("matrix", &Rigid3d::ToMatrix) 82 | .def("essential_matrix", &EssentialMatrixFromPose) 83 | .def(py::self * Rigid3d()) 84 | .def(py::self * Eigen::Vector3d()) 85 | .def("__mul__", 86 | [](const Rigid3d& t, 87 | const py::EigenDRef& points) 88 | -> Eigen::MatrixX3d { 89 | return (points * t.rotation.toRotationMatrix().transpose()) 90 | .rowwise() + 91 | t.translation.transpose(); 92 | }) 93 | .def("inverse", static_cast(&Inverse)) 94 | .def_static("interpolate", &InterpolateCameraPoses) 95 | .def("__repr__", [](const Rigid3d& self) { 96 | std::stringstream ss; 97 | ss << "Rigid3d(" 98 | << "quat_xyzw=[" << self.rotation.coeffs().format(vec_fmt) << "], " 99 | << "t=[" << self.translation.format(vec_fmt) << "])"; 100 | return ss.str(); 101 | }); 102 | py::implicitly_convertible(); 103 | MakeDataclass(PyRigid3d); 104 | 105 | py::class_ext_ PySim3d(m, "Sim3d"); 106 | PySim3d.def(py::init<>()) 107 | .def( 108 | py::init()) 109 | .def(py::init(&Sim3d::FromMatrix)) 110 | .def_readwrite("scale", &Sim3d::scale) 111 | .def_readwrite("rotation", &Sim3d::rotation) 112 | .def_readwrite("translation", &Sim3d::translation) 113 | .def("matrix", &Sim3d::ToMatrix) 114 | .def(py::self * Sim3d()) 115 | .def(py::self * Eigen::Vector3d()) 116 | .def("__mul__", 117 | [](const Sim3d& t, 118 | const py::EigenDRef& points) 119 | -> Eigen::MatrixX3d { 120 | return (t.scale * 121 | (points * t.rotation.toRotationMatrix().transpose())) 122 | .rowwise() + 123 | t.translation.transpose(); 124 | }) 125 | .def("transform_camera_world", &TransformCameraWorld) 126 | .def("inverse", static_cast(&Inverse)) 127 | .def("__repr__", [](const Sim3d& self) { 128 | std::stringstream ss; 129 | ss << "Sim3d(" 130 | << "scale=" << self.scale << ", " 131 | << "quat_xyzw=[" << self.rotation.coeffs().format(vec_fmt) << "], " 132 | << "t=[" << self.translation.format(vec_fmt) << "])"; 133 | return ss.str(); 134 | }); 135 | py::implicitly_convertible(); 136 | MakeDataclass(PySim3d); 137 | } 138 | -------------------------------------------------------------------------------- /pycolmap/geometry/homography_matrix.h: -------------------------------------------------------------------------------- 1 | #include "colmap/geometry/homography_matrix.h" 2 | 3 | #include "pycolmap/log_exceptions.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace colmap; 10 | using namespace pybind11::literals; 11 | namespace py = pybind11; 12 | 13 | py::dict PyPoseFromHomographyMatrix( 14 | const Eigen::Matrix3d& H, 15 | const Eigen::Matrix3d& K1, 16 | const Eigen::Matrix3d& K2, 17 | const std::vector& points1, 18 | const std::vector& points2) { 19 | THROW_CHECK_EQ(points1.size(), points1.size()); 20 | py::gil_scoped_release release; 21 | 22 | Eigen::Matrix3d R; 23 | Eigen::Vector3d t; 24 | Eigen::Vector3d n; 25 | std::vector points3D; 26 | PoseFromHomographyMatrix(H, K1, K2, points1, points2, &R, &t, &n, &points3D); 27 | 28 | py::gil_scoped_acquire acquire; 29 | return py::dict("R"_a = R, "t"_a = t, "n"_a = n, "points3D"_a = points3D); 30 | } 31 | 32 | void BindHomographyGeometry(py::module& m) { 33 | m.def("homography_decomposition", 34 | &PyPoseFromHomographyMatrix, 35 | "H"_a, 36 | "K1"_a, 37 | "K2"_a, 38 | "points1"_a, 39 | "points2"_a, 40 | "Analytical Homography Decomposition."); 41 | } 42 | -------------------------------------------------------------------------------- /pycolmap/helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "colmap/util/string.h" 4 | #include "colmap/util/threading.h" 5 | 6 | #include "pycolmap/log_exceptions.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | using namespace colmap; 23 | using namespace pybind11::literals; 24 | namespace py = pybind11; 25 | 26 | const Eigen::IOFormat vec_fmt(Eigen::StreamPrecision, 27 | Eigen::DontAlignCols, 28 | ", ", 29 | ", "); 30 | 31 | template 32 | T pyStringToEnum(const py::enum_& enm, const std::string& value) { 33 | const auto values = enm.attr("__members__").template cast(); 34 | const auto str_val = py::str(value); 35 | if (values.contains(str_val)) { 36 | return T(values[str_val].template cast()); 37 | } 38 | const std::string msg = 39 | "Invalid string value " + value + " for enum " + 40 | std::string(enm.attr("__name__").template cast()); 41 | THROW_EXCEPTION(std::out_of_range, msg.c_str()); 42 | return T(); 43 | } 44 | 45 | template 46 | void AddStringToEnumConstructor(py::enum_& enm) { 47 | enm.def(py::init([enm](const std::string& value) { 48 | return pyStringToEnum(enm, py::str(value)); // str constructor 49 | })); 50 | py::implicitly_convertible(); 51 | } 52 | 53 | void UpdateFromDict(py::object& self, const py::dict& dict) { 54 | for (const auto& it : dict) { 55 | if (!py::isinstance(it.first)) { 56 | const std::string msg = "Dictionary key is not a string: " + 57 | py::str(it.first).template cast(); 58 | THROW_EXCEPTION(std::invalid_argument, msg.c_str()); 59 | } 60 | const py::str name = py::reinterpret_borrow(it.first); 61 | const py::handle& value = it.second; 62 | const auto attr = self.attr(name); 63 | try { 64 | if (py::hasattr(attr, "mergedict") && py::isinstance(value)) { 65 | attr.attr("mergedict").attr("__call__")(value); 66 | } else { 67 | self.attr(name) = value; 68 | } 69 | } catch (const py::error_already_set& ex) { 70 | if (ex.matches(PyExc_TypeError)) { 71 | // If fail we try bases of the class 72 | const py::list bases = 73 | attr.attr("__class__").attr("__bases__").cast(); 74 | bool success_on_base = false; 75 | for (auto& base : bases) { 76 | try { 77 | self.attr(name) = base(value); 78 | success_on_base = true; 79 | break; 80 | } catch (const py::error_already_set&) { 81 | continue; // We anyway throw afterwards 82 | } 83 | } 84 | if (success_on_base) { 85 | continue; 86 | } 87 | std::stringstream ss; 88 | ss << self.attr("__class__") 89 | .attr("__name__") 90 | .template cast() 91 | << "." << name.template cast() << ": Could not convert " 92 | << py::type::of(value.cast()) 93 | .attr("__name__") 94 | .template cast() 95 | << ": " << py::str(value).template cast() << " to '" 96 | << py::type::of(attr).attr("__name__").template cast() 97 | << "'."; 98 | // We write the err message to give info even if exceptions 99 | // is catched outside, e.g. in function overload resolve 100 | LOG(ERROR) << "Internal TypeError: " << ss.str(); 101 | throw(py::type_error(std::string("Failed to merge dict into class: ") + 102 | "Could not assign " + 103 | name.template cast())); 104 | } else if (ex.matches(PyExc_AttributeError) && 105 | py::str(ex.value()).cast() == 106 | std::string("can't set attribute")) { 107 | std::stringstream ss; 108 | ss << self.attr("__class__") 109 | .attr("__name__") 110 | .template cast() 111 | << "." << name.template cast() << " defined readonly."; 112 | throw py::attribute_error(ss.str()); 113 | } else if (ex.matches(PyExc_AttributeError)) { 114 | LOG(ERROR) << "Internal AttributeError: " 115 | << py::str(ex.value()).cast(); 116 | throw; 117 | } else { 118 | LOG(ERROR) << "Internal Error: " 119 | << py::str(ex.value()).cast(); 120 | throw; 121 | } 122 | } 123 | } 124 | } 125 | 126 | bool AttributeIsFunction(const std::string& name, const py::object& value) { 127 | return (name.find("__") == 0 || name.rfind("__") != std::string::npos || 128 | py::hasattr(value, "__func__") || py::hasattr(value, "__call__")); 129 | } 130 | 131 | std::vector ListObjectAttributes(const py::object& pyself) { 132 | std::vector attributes; 133 | for (const auto& handle : pyself.attr("__dir__")()) { 134 | const py::str attribute = py::reinterpret_borrow(handle); 135 | const auto value = pyself.attr(attribute); 136 | if (AttributeIsFunction(attribute, value)) { 137 | continue; 138 | } 139 | attributes.push_back(attribute); 140 | } 141 | return attributes; 142 | } 143 | 144 | template 145 | py::dict ConvertToDict(const T& self, 146 | std::vector attributes, 147 | const bool recursive) { 148 | const py::object pyself = py::cast(self); 149 | if (attributes.empty()) { 150 | attributes = ListObjectAttributes(pyself); 151 | } 152 | py::dict dict; 153 | for (const auto& attr : attributes) { 154 | const auto value = pyself.attr(attr.c_str()); 155 | if (recursive && py::hasattr(value, "todict")) { 156 | dict[attr.c_str()] = 157 | value.attr("todict").attr("__call__")().template cast(); 158 | } else { 159 | dict[attr.c_str()] = value; 160 | } 161 | } 162 | return dict; 163 | } 164 | 165 | template 166 | std::string CreateSummary(const T& self, bool write_type) { 167 | std::stringstream ss; 168 | auto pyself = py::cast(self); 169 | const std::string prefix = " "; 170 | bool after_subsummary = false; 171 | ss << pyself.attr("__class__").attr("__name__").template cast() 172 | << ":"; 173 | for (auto& handle : pyself.attr("__dir__")()) { 174 | const py::str name = py::reinterpret_borrow(handle); 175 | py::object attribute; 176 | try { 177 | attribute = pyself.attr(name); 178 | } catch (const std::exception& e) { 179 | // Some properties are not valid for some uninitialized objects. 180 | continue; 181 | } 182 | if (AttributeIsFunction(name, attribute)) { 183 | continue; 184 | } 185 | ss << "\n"; 186 | if (!after_subsummary) { 187 | ss << prefix; 188 | } 189 | ss << name.template cast(); 190 | if (py::hasattr(attribute, "summary")) { 191 | std::string summ = attribute.attr("summary") 192 | .attr("__call__")(write_type) 193 | .template cast(); 194 | // NOLINTNEXTLINE(performance-inefficient-string-concatenation) 195 | summ = std::regex_replace(summ, std::regex("\n"), "\n" + prefix); 196 | ss << ": " << summ; 197 | } else { 198 | if (write_type) { 199 | const std::string type_str = 200 | py::str(py::type::of(attribute).attr("__name__")); 201 | ss << ": " << type_str; 202 | after_subsummary = true; 203 | } 204 | std::string value = py::str(attribute); 205 | if (value.length() > 80 && py::hasattr(attribute, "__len__")) { 206 | const int length = attribute.attr("__len__")().template cast(); 207 | value = StringPrintf( 208 | "%c ... %d elements ... %c", value.front(), length, value.back()); 209 | } 210 | ss << " = " << value; 211 | after_subsummary = false; 212 | } 213 | } 214 | return ss.str(); 215 | } 216 | 217 | template 218 | void AddDefaultsToDocstrings(py::class_ cls) { 219 | auto obj = cls(); 220 | for (auto& handle : obj.attr("__dir__")()) { 221 | const std::string attribute = py::str(handle); 222 | py::object member; 223 | try { 224 | member = obj.attr(attribute.c_str()); 225 | } catch (const std::exception& e) { 226 | // Some properties are not valid for some uninitialized objects. 227 | continue; 228 | } 229 | auto prop = cls.attr(attribute.c_str()); 230 | if (AttributeIsFunction(attribute, member)) { 231 | continue; 232 | } 233 | const auto type_name = py::type::of(member).attr("__name__"); 234 | const std::string doc = 235 | StringPrintf("%s (%s, default: %s)", 236 | py::str(prop.doc()).cast().c_str(), 237 | type_name.template cast().c_str(), 238 | py::str(member).cast().c_str()); 239 | prop.doc() = py::str(doc); 240 | } 241 | } 242 | 243 | template 244 | void MakeDataclass(py::class_ cls, 245 | const std::vector& attributes = {}) { 246 | AddDefaultsToDocstrings(cls); 247 | if (!py::hasattr(cls, "summary")) { 248 | cls.def("summary", &CreateSummary, "write_type"_a = false); 249 | } 250 | cls.def("mergedict", &UpdateFromDict); 251 | cls.def( 252 | "todict", 253 | [attributes](const T& self, const bool recursive) { 254 | return ConvertToDict(self, attributes, recursive); 255 | }, 256 | "recursive"_a = true); 257 | 258 | cls.def(py::init([cls](const py::dict& dict) { 259 | py::object self = cls(); 260 | self.attr("mergedict").attr("__call__")(dict); 261 | return self.cast(); 262 | })); 263 | cls.def(py::init([cls](const py::kwargs& kwargs) { 264 | py::dict dict = kwargs.cast(); 265 | return cls(dict).template cast(); 266 | })); 267 | py::implicitly_convertible(); 268 | py::implicitly_convertible(); 269 | 270 | cls.def("__copy__", [](const T& self) { return T(self); }); 271 | cls.def("__deepcopy__", 272 | [](const T& self, const py::dict&) { return T(self); }); 273 | 274 | cls.def(py::pickle( 275 | [attributes](const T& self) { 276 | return ConvertToDict(self, attributes, /*recursive=*/false); 277 | }, 278 | [cls](const py::dict& dict) { 279 | py::object self = cls(); 280 | self.attr("mergedict").attr("__call__")(dict); 281 | return self.cast(); 282 | })); 283 | } 284 | 285 | // Catch python keyboard interrupts 286 | 287 | /* 288 | // single 289 | if (PyInterrupt().Raised()) { 290 | // stop the execution and raise an exception 291 | throw py::error_already_set(); 292 | } 293 | 294 | // loop 295 | PyInterrupt py_interrupt = PyInterrupt(2.0) 296 | for (...) { 297 | if (py_interrupt.Raised()) { 298 | // stop the execution and raise an exception 299 | throw py::error_already_set(); 300 | } 301 | // Do your workload here 302 | } 303 | 304 | 305 | */ 306 | struct PyInterrupt { 307 | using clock = std::chrono::steady_clock; 308 | using sec = std::chrono::duration; 309 | explicit PyInterrupt(double gap = -1.0); 310 | 311 | inline bool Raised(); 312 | 313 | private: 314 | std::mutex mutex_; 315 | bool found = false; 316 | Timer timer_; 317 | clock::time_point start; 318 | double gap_; 319 | }; 320 | 321 | PyInterrupt::PyInterrupt(double gap) : gap_(gap), start(clock::now()) {} 322 | 323 | bool PyInterrupt::Raised() { 324 | const sec duration = clock::now() - start; 325 | if (!found && duration.count() > gap_) { 326 | std::lock_guard lock(mutex_); 327 | py::gil_scoped_acquire acq; 328 | found = (PyErr_CheckSignals() != 0); 329 | start = clock::now(); 330 | } 331 | return found; 332 | } 333 | 334 | // Instead of thread.Wait() call this to allow interrupts through python 335 | void PyWait(Thread* thread, double gap = 2.0) { 336 | PyInterrupt py_interrupt(gap); 337 | while (thread->IsRunning()) { 338 | if (py_interrupt.Raised()) { 339 | LOG(ERROR) << "Stopping thread..."; 340 | thread->Stop(); 341 | thread->Wait(); 342 | throw py::error_already_set(); 343 | } 344 | } 345 | // after finishing join the thread to avoid abort 346 | thread->Wait(); 347 | } 348 | -------------------------------------------------------------------------------- /pycolmap/log_exceptions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "colmap/util/misc.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | namespace py = pybind11; 11 | 12 | template 13 | inline std::string ToString(T msg) { 14 | return std::to_string(msg); 15 | } 16 | 17 | inline std::string ToString(std::string msg) { return msg; } 18 | 19 | inline std::string ToString(const char* msg) { return std::string(msg); } 20 | 21 | inline const char* __ColmapGetConstFileBaseName(const char* file) { 22 | const char* base = strrchr(file, '/'); 23 | if (!base) { 24 | base = strrchr(file, '\\'); 25 | } 26 | return base ? (base + 1) : file; 27 | } 28 | 29 | template 30 | inline T TemplateException(const char* file, 31 | const int line, 32 | const std::string& txt) { 33 | std::stringstream ss; 34 | ss << "[" << __ColmapGetConstFileBaseName(file) << ":" << line << "] " << txt; 35 | return T(ss.str()); 36 | } 37 | 38 | inline std::string __GetConditionString(const char* cond_str) { 39 | std::stringstream ss; 40 | ss << "Condition Failed: " << cond_str; 41 | return ss.str(); 42 | } 43 | 44 | inline std::string __GetCheckString(const char* cond_str) { 45 | std::stringstream ss; 46 | ss << "Check Failed: " << cond_str; 47 | return ss.str(); 48 | } 49 | 50 | inline std::string __MergeTwoConstChar(const char* expr1, const char* expr2) { 51 | return (std::string(expr1) + std::string(" ") + expr2); 52 | } 53 | 54 | inline void __ThrowCheckImpl(const char* file, 55 | const int line, 56 | const bool result, 57 | const char* expr_str) { 58 | if (!result) { 59 | throw TemplateException( 60 | file, line, __GetCheckString(expr_str).c_str()); 61 | } 62 | } 63 | 64 | inline void __ThrowCheckImplMsg(const char* file, 65 | const int line, 66 | const bool result, 67 | const char* expr_str, 68 | const std::string& msg) { 69 | if (!result) { 70 | std::stringstream ss; 71 | ss << expr_str << " : " << msg; 72 | std::string m = ss.str(); 73 | throw TemplateException( 74 | file, line, __GetCheckString(m.c_str())); 75 | } 76 | } 77 | 78 | template 79 | void __ThrowCheckOpImpl(const char* file, 80 | const int line, 81 | const bool result, 82 | const T1& val1, 83 | const T2& val2, 84 | const char* val1_str, 85 | const char* val2_str, 86 | const char* op_str) { 87 | if (!result) { 88 | std::stringstream ss; 89 | ss << val1_str << " " << op_str << " " << val2_str << " (" << val1 90 | << " vs. " << val2 << ")"; 91 | std::string msg = ss.str(); 92 | throw TemplateException( 93 | file, line, __GetCheckString(msg.c_str())); 94 | } 95 | } 96 | 97 | // Option checker macros. In contrast to glog, this function does not abort the 98 | // program, but simply throws an exception on failure. 99 | #define THROW_EXCEPTION(exception, msg) \ 100 | throw TemplateException(__FILE__, __LINE__, ToString(msg)); 101 | 102 | #define THROW_CUSTOM_CHECK_MSG(condition, exception, msg) \ 103 | if (!(condition)) \ 104 | throw TemplateException( \ 105 | __FILE__, \ 106 | __LINE__, \ 107 | __GetCheckString(#condition) + std::string(" ") + ToString(msg)); 108 | 109 | #define THROW_CUSTOM_CHECK(condition, exception) \ 110 | if (!(condition)) \ 111 | throw TemplateException( \ 112 | __FILE__, __LINE__, __GetCheckString(#condition)); 113 | 114 | #define THROW_CHECK(expr) __ThrowCheckImpl(__FILE__, __LINE__, (expr), #expr); 115 | 116 | #define THROW_CHECK_MSG(expr, msg) \ 117 | __ThrowCheckImplMsg(__FILE__, __LINE__, (expr), #expr, ToString(msg)) 118 | 119 | #define THROW_CHECK_OP(name, op, val1, val2) \ 120 | __ThrowCheckOpImpl( \ 121 | __FILE__, __LINE__, (val1 op val2), val1, val2, #val1, #val2, #op); 122 | 123 | #define THROW_CHECK_EQ(val1, val2) THROW_CHECK_OP(_EQ, ==, val1, val2) 124 | #define THROW_CHECK_NE(val1, val2) THROW_CHECK_OP(_NE, !=, val1, val2) 125 | #define THROW_CHECK_LE(val1, val2) THROW_CHECK_OP(_LE, <=, val1, val2) 126 | #define THROW_CHECK_LT(val1, val2) THROW_CHECK_OP(_LT, <, val1, val2) 127 | #define THROW_CHECK_GE(val1, val2) THROW_CHECK_OP(_GE, >=, val1, val2) 128 | #define THROW_CHECK_GT(val1, val2) THROW_CHECK_OP(_GT, >, val1, val2) 129 | 130 | #define THROW_CHECK_FILE_EXISTS(path) \ 131 | THROW_CHECK_MSG(ExistsFile(path), \ 132 | std::string("File ") + (path) + " does not exist."); 133 | 134 | #define THROW_CHECK_DIR_EXISTS(path) \ 135 | THROW_CHECK_MSG(ExistsDir(path), \ 136 | std::string("Directory ") + (path) + " does not exist."); 137 | 138 | #define THROW_CHECK_FILE_OPEN(path) \ 139 | THROW_CHECK_MSG( \ 140 | std::ofstream(path, std::ios::trunc).is_open(), \ 141 | std::string(": Could not open ") + (path) + \ 142 | ". Is the path a directory or does the parent dir not exist?"); 143 | 144 | #define THROW_CHECK_HAS_FILE_EXTENSION(path, ext) \ 145 | THROW_CHECK_MSG(HasFileExtension(path, ext), \ 146 | std::string("Path ") + (path) + \ 147 | " does not match file extension " + (ext) + "."); 148 | -------------------------------------------------------------------------------- /pycolmap/main.cc: -------------------------------------------------------------------------------- 1 | #include "colmap/util/version.h" 2 | 3 | #include "pycolmap/estimators/bindings.h" 4 | #include "pycolmap/feature/sift.h" 5 | #include "pycolmap/geometry/bindings.h" 6 | #include "pycolmap/helpers.h" 7 | #include "pycolmap/optim/bindings.h" 8 | #include "pycolmap/pipeline/bindings.h" 9 | #include "pycolmap/pybind11_extension.h" 10 | #include "pycolmap/scene/bindings.h" 11 | #include "pycolmap/sfm/bindings.h" 12 | #include "pycolmap/utils.h" 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | namespace py = pybind11; 19 | using namespace colmap; 20 | 21 | struct Logging { 22 | // TODO: Replace with google::LogSeverity in glog >= v0.7.0 23 | enum class LogSeverity { 24 | GLOG_INFO = google::GLOG_INFO, 25 | GLOG_WARNING = google::GLOG_WARNING, 26 | GLOG_ERROR = google::GLOG_ERROR, 27 | GLOG_FATAL = google::GLOG_FATAL, 28 | }; 29 | }; // dummy class 30 | 31 | std::pair GetPythonCallFrame() { 32 | const auto frame = py::module_::import("sys").attr("_getframe")(0); 33 | const std::string file = py::str(frame.attr("f_code").attr("co_filename")); 34 | const std::string function = py::str(frame.attr("f_code").attr("co_name")); 35 | const int line = py::int_(frame.attr("f_lineno")); 36 | return std::make_pair(file + ":" + function, line); 37 | } 38 | 39 | void BindLogging(py::module& m) { 40 | py::class_ PyLogging(m, "logging"); 41 | PyLogging.def_readwrite_static("minloglevel", &FLAGS_minloglevel) 42 | .def_readwrite_static("stderrthreshold", &FLAGS_stderrthreshold) 43 | .def_readwrite_static("log_dir", &FLAGS_log_dir) 44 | .def_readwrite_static("logtostderr", &FLAGS_logtostderr) 45 | .def_readwrite_static("alsologtostderr", &FLAGS_alsologtostderr) 46 | .def_static( 47 | "set_log_destination", 48 | [](const Logging::LogSeverity severity, const std::string& path) { 49 | google::SetLogDestination( 50 | static_cast(severity), path.c_str()); 51 | }) 52 | .def_static( 53 | "info", 54 | [](const std::string& msg) { 55 | auto frame = GetPythonCallFrame(); 56 | google::LogMessage(frame.first.c_str(), frame.second).stream() 57 | << msg; 58 | }) 59 | .def_static("warning", 60 | [](const std::string& msg) { 61 | auto frame = GetPythonCallFrame(); 62 | google::LogMessage( 63 | frame.first.c_str(), frame.second, google::GLOG_WARNING) 64 | .stream() 65 | << msg; 66 | }) 67 | .def_static("error", 68 | [](const std::string& msg) { 69 | auto frame = GetPythonCallFrame(); 70 | google::LogMessage( 71 | frame.first.c_str(), frame.second, google::GLOG_ERROR) 72 | .stream() 73 | << msg; 74 | }) 75 | .def_static("fatal", [](const std::string& msg) { 76 | auto frame = GetPythonCallFrame(); 77 | google::LogMessageFatal(frame.first.c_str(), frame.second).stream() 78 | << msg; 79 | }); 80 | py::enum_(PyLogging, "Level") 81 | .value("INFO", Logging::LogSeverity::GLOG_INFO) 82 | .value("WARNING", Logging::LogSeverity::GLOG_WARNING) 83 | .value("ERROR", Logging::LogSeverity::GLOG_ERROR) 84 | .value("FATAL", Logging::LogSeverity::GLOG_FATAL) 85 | .export_values(); 86 | google::InitGoogleLogging(""); 87 | google::InstallFailureSignalHandler(); 88 | FLAGS_alsologtostderr = true; 89 | } 90 | 91 | PYBIND11_MODULE(pycolmap, m) { 92 | m.doc() = "COLMAP plugin"; 93 | #ifdef VERSION_INFO 94 | m.attr("__version__") = py::str(VERSION_INFO); 95 | #else 96 | m.attr("__version__") = py::str("dev"); 97 | #endif 98 | m.attr("has_cuda") = IsGPU(Device::AUTO); 99 | m.attr("COLMAP_version") = py::str(GetVersionInfo()); 100 | m.attr("COLMAP_build") = py::str(GetBuildInfo()); 101 | 102 | auto PyDevice = py::enum_(m, "Device") 103 | .value("auto", Device::AUTO) 104 | .value("cpu", Device::CPU) 105 | .value("cuda", Device::CUDA); 106 | AddStringToEnumConstructor(PyDevice); 107 | 108 | BindLogging(m); 109 | BindGeometry(m); 110 | BindOptim(m); 111 | BindScene(m); 112 | BindEstimators(m); 113 | BindSfMObjects(m); 114 | BindSift(m); 115 | BindPipeline(m); 116 | 117 | py::add_ostream_redirect(m, "ostream"); 118 | } 119 | -------------------------------------------------------------------------------- /pycolmap/optim/bindings.h: -------------------------------------------------------------------------------- 1 | #include "colmap/optim/ransac.h" 2 | 3 | #include 4 | 5 | namespace py = pybind11; 6 | 7 | void BindOptim(py::module& m) { 8 | auto PyRANSACOptions = 9 | py::class_(m, "RANSACOptions") 10 | .def(py::init<>([]() { 11 | RANSACOptions options; 12 | options.max_error = 4.0; 13 | options.min_inlier_ratio = 0.01; 14 | options.confidence = 0.9999; 15 | options.min_num_trials = 1000; 16 | options.max_num_trials = 100000; 17 | return options; 18 | })) 19 | .def_readwrite("max_error", &RANSACOptions::max_error) 20 | .def_readwrite("min_inlier_ratio", &RANSACOptions::min_inlier_ratio) 21 | .def_readwrite("confidence", &RANSACOptions::confidence) 22 | .def_readwrite("dyn_num_trials_multiplier", 23 | &RANSACOptions::dyn_num_trials_multiplier) 24 | .def_readwrite("min_num_trials", &RANSACOptions::min_num_trials) 25 | .def_readwrite("max_num_trials", &RANSACOptions::max_num_trials); 26 | MakeDataclass(PyRANSACOptions); 27 | } 28 | -------------------------------------------------------------------------------- /pycolmap/pipeline/bindings.h: -------------------------------------------------------------------------------- 1 | #include "pycolmap/pipeline/extract_features.h" 2 | #include "pycolmap/pipeline/images.h" 3 | #include "pycolmap/pipeline/match_features.h" 4 | #include "pycolmap/pipeline/meshing.h" 5 | #include "pycolmap/pipeline/mvs.h" 6 | #include "pycolmap/pipeline/sfm.h" 7 | 8 | #include 9 | 10 | namespace py = pybind11; 11 | 12 | void BindPipeline(py::module& m) { 13 | BindImages(m); 14 | BindExtractFeatures(m); 15 | BindMatchFeatures(m); 16 | BindSfM(m); 17 | BindMVS(m); 18 | BindMeshing(m); 19 | } 20 | -------------------------------------------------------------------------------- /pycolmap/pipeline/extract_features.h: -------------------------------------------------------------------------------- 1 | #include "colmap/controllers/feature_extraction.h" 2 | #include "colmap/controllers/image_reader.h" 3 | #include "colmap/exe/feature.h" 4 | #include "colmap/exe/sfm.h" 5 | #include "colmap/feature/sift.h" 6 | #include "colmap/util/misc.h" 7 | 8 | #include "pycolmap/helpers.h" 9 | #include "pycolmap/log_exceptions.h" 10 | #include "pycolmap/utils.h" 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | using namespace colmap; 20 | using namespace pybind11::literals; 21 | namespace py = pybind11; 22 | 23 | void ExtractFeatures(const std::string& database_path, 24 | const std::string& image_path, 25 | const std::vector& image_list, 26 | const CameraMode camera_mode, 27 | const std::string& camera_model, 28 | ImageReaderOptions reader_options, 29 | SiftExtractionOptions sift_options, 30 | const Device device) { 31 | THROW_CHECK_MSG(!ExistsFile(database_path), 32 | database_path + " already exists."); 33 | THROW_CHECK_HAS_FILE_EXTENSION(database_path, ".db"); 34 | THROW_CHECK_FILE_OPEN(database_path); 35 | THROW_CHECK_DIR_EXISTS(image_path); 36 | sift_options.use_gpu = IsGPU(device); 37 | VerifyGPUParams(sift_options.use_gpu); 38 | 39 | UpdateImageReaderOptionsFromCameraMode(reader_options, camera_mode); 40 | reader_options.camera_model = camera_model; 41 | 42 | reader_options.database_path = database_path; 43 | reader_options.image_path = image_path; 44 | 45 | if (!image_list.empty()) { 46 | reader_options.image_list = image_list; 47 | } 48 | 49 | THROW_CHECK(ExistsCameraModelWithName(reader_options.camera_model)); 50 | 51 | THROW_CUSTOM_CHECK_MSG(VerifyCameraParams(reader_options.camera_model, 52 | reader_options.camera_params), 53 | std::invalid_argument, 54 | "Invalid camera parameters."); 55 | 56 | py::gil_scoped_release release; 57 | std::unique_ptr extractor = 58 | CreateFeatureExtractorController(reader_options, sift_options); 59 | extractor->Start(); 60 | PyWait(extractor.get()); 61 | } 62 | 63 | void BindExtractFeatures(py::module& m) { 64 | using SEOpts = SiftExtractionOptions; 65 | auto PyNormalization = 66 | py::enum_(m, "Normalization") 67 | .value("L1_ROOT", 68 | SEOpts::Normalization::L1_ROOT, 69 | "L1-normalizes each descriptor followed by element-wise " 70 | "square rooting. This normalization is usually better than " 71 | "standard " 72 | "L2-normalization. See 'Three things everyone should know " 73 | "to improve object retrieval', Relja Arandjelovic and " 74 | "Andrew Zisserman, CVPR 2012.") 75 | .value( 76 | "L2", SEOpts::Normalization::L2, "Each vector is L2-normalized."); 77 | AddStringToEnumConstructor(PyNormalization); 78 | auto PySiftExtractionOptions = 79 | py::class_(m, "SiftExtractionOptions") 80 | .def(py::init<>()) 81 | .def_readwrite("num_threads", 82 | &SEOpts::num_threads, 83 | "Number of threads for feature matching and " 84 | "geometric verification.") 85 | .def_readwrite("gpu_index", 86 | &SEOpts::gpu_index, 87 | "Index of the GPU used for feature matching. For " 88 | "multi-GPU matching, you should separate multiple " 89 | "GPU indices by comma, e.g., '0,1,2,3'.") 90 | .def_readwrite( 91 | "max_image_size", 92 | &SEOpts::max_image_size, 93 | "Maximum image size, otherwise image will be down-scaled.") 94 | .def_readwrite("max_num_features", 95 | &SEOpts::max_num_features, 96 | "Maximum number of features to detect, keeping " 97 | "larger-scale features.") 98 | .def_readwrite("first_octave", 99 | &SEOpts::first_octave, 100 | "First octave in the pyramid, i.e. -1 upsamples the " 101 | "image by one level.") 102 | .def_readwrite("num_octaves", &SEOpts::num_octaves) 103 | .def_readwrite("octave_resolution", 104 | &SEOpts::octave_resolution, 105 | "Number of levels per octave.") 106 | .def_readwrite("peak_threshold", 107 | &SEOpts::peak_threshold, 108 | "Peak threshold for detection.") 109 | .def_readwrite("edge_threshold", 110 | &SEOpts::edge_threshold, 111 | "Edge threshold for detection.") 112 | .def_readwrite("estimate_affine_shape", 113 | &SEOpts::estimate_affine_shape, 114 | "Estimate affine shape of SIFT features in the form " 115 | "of oriented ellipses as opposed to original SIFT " 116 | "which estimates oriented disks.") 117 | .def_readwrite("max_num_orientations", 118 | &SEOpts::max_num_orientations, 119 | "Maximum number of orientations per keypoint if not " 120 | "estimate_affine_shape.") 121 | .def_readwrite("upright", 122 | &SEOpts::upright, 123 | "Fix the orientation to 0 for upright features") 124 | .def_readwrite("darkness_adaptivity", 125 | &SEOpts::darkness_adaptivity, 126 | "Whether to adapt the feature detection depending " 127 | "on the image darkness. only available on GPU.") 128 | .def_readwrite( 129 | "domain_size_pooling", 130 | &SEOpts::domain_size_pooling, 131 | "\"Domain-Size Pooling in Local Descriptors and Network" 132 | "Architectures\", J. Dong and S. Soatto, CVPR 2015") 133 | .def_readwrite("dsp_min_scale", &SEOpts::dsp_min_scale) 134 | .def_readwrite("dsp_max_scale", &SEOpts::dsp_max_scale) 135 | .def_readwrite("dsp_num_scales", &SEOpts::dsp_num_scales) 136 | .def_readwrite("normalization", 137 | &SEOpts::normalization, 138 | "L1_ROOT or L2 descriptor normalization"); 139 | MakeDataclass(PySiftExtractionOptions); 140 | auto sift_extraction_options = PySiftExtractionOptions().cast(); 141 | 142 | /* PIPELINE */ 143 | m.def("extract_features", 144 | &ExtractFeatures, 145 | "database_path"_a, 146 | "image_path"_a, 147 | "image_list"_a = std::vector(), 148 | "camera_mode"_a = CameraMode::AUTO, 149 | "camera_model"_a = "SIMPLE_RADIAL", 150 | "reader_options"_a = ImageReaderOptions(), 151 | "sift_options"_a = sift_extraction_options, 152 | "device"_a = Device::AUTO, 153 | "Extract SIFT Features and write them to database"); 154 | } 155 | -------------------------------------------------------------------------------- /pycolmap/pipeline/images.h: -------------------------------------------------------------------------------- 1 | #include "colmap/controllers/image_reader.h" 2 | #include "colmap/exe/feature.h" 3 | #include "colmap/feature/sift.h" 4 | #include "colmap/image/undistortion.h" 5 | #include "colmap/scene/camera.h" 6 | #include "colmap/scene/reconstruction.h" 7 | #include "colmap/util/misc.h" 8 | 9 | #include "pycolmap/helpers.h" 10 | #include "pycolmap/log_exceptions.h" 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | using namespace colmap; 21 | using namespace pybind11::literals; 22 | namespace py = pybind11; 23 | 24 | void ImportImages(const std::string& database_path, 25 | const std::string& image_path, 26 | const CameraMode camera_mode, 27 | const std::vector& image_list, 28 | const ImageReaderOptions& options_) { 29 | THROW_CHECK_FILE_EXISTS(database_path); 30 | THROW_CHECK_DIR_EXISTS(image_path); 31 | 32 | ImageReaderOptions options(options_); 33 | options.database_path = database_path; 34 | options.image_path = image_path; 35 | options.image_list = image_list; 36 | UpdateImageReaderOptionsFromCameraMode(options, camera_mode); 37 | THROW_CUSTOM_CHECK_MSG( 38 | ExistsCameraModelWithName(options.camera_model), 39 | std::invalid_argument, 40 | (std::string("Invalid camera model: ") + options.camera_model).c_str()); 41 | 42 | Database database(options.database_path); 43 | ImageReader image_reader(options, &database); 44 | 45 | PyInterrupt py_interrupt(2.0); 46 | 47 | while (image_reader.NextIndex() < image_reader.NumImages()) { 48 | if (py_interrupt.Raised()) { 49 | throw py::error_already_set(); 50 | } 51 | Camera camera; 52 | Image image; 53 | Bitmap bitmap; 54 | if (image_reader.Next(&camera, &image, &bitmap, nullptr) != 55 | ImageReader::Status::SUCCESS) { 56 | continue; 57 | } 58 | DatabaseTransaction database_transaction(&database); 59 | if (image.ImageId() == kInvalidImageId) { 60 | image.SetImageId(database.WriteImage(image)); 61 | } 62 | } 63 | } 64 | 65 | Camera InferCameraFromImage(const std::string& image_path, 66 | const ImageReaderOptions& options) { 67 | THROW_CHECK_FILE_EXISTS(image_path); 68 | 69 | Bitmap bitmap; 70 | THROW_CUSTOM_CHECK_MSG( 71 | bitmap.Read(image_path, false), 72 | std::invalid_argument, 73 | (std::string("Cannot read image file: ") + image_path).c_str()); 74 | 75 | double focal_length = 0.0; 76 | bool has_prior_focal_length = bitmap.ExifFocalLength(&focal_length); 77 | if (!has_prior_focal_length) { 78 | focal_length = options.default_focal_length_factor * 79 | std::max(bitmap.Width(), bitmap.Height()); 80 | } 81 | Camera camera = Camera::CreateFromModelName(kInvalidCameraId, 82 | options.camera_model, 83 | focal_length, 84 | bitmap.Width(), 85 | bitmap.Height()); 86 | camera.has_prior_focal_length = has_prior_focal_length; 87 | THROW_CUSTOM_CHECK_MSG( 88 | camera.VerifyParams(), 89 | std::invalid_argument, 90 | (std::string("Invalid camera params: ") + camera.ParamsToString()) 91 | .c_str()); 92 | 93 | return camera; 94 | } 95 | 96 | void UndistortImages(const std::string& output_path, 97 | const std::string& input_path, 98 | const std::string& image_path, 99 | const std::vector& image_list, 100 | const std::string& output_type, 101 | const CopyType copy_type, 102 | const int num_patch_match_src_images, 103 | const UndistortCameraOptions& undistort_camera_options) { 104 | THROW_CHECK_DIR_EXISTS(input_path); 105 | THROW_CHECK_DIR_EXISTS(image_path); 106 | 107 | CreateDirIfNotExists(output_path); 108 | Reconstruction reconstruction; 109 | reconstruction.Read(input_path); 110 | LOG(INFO) << StringPrintf(" => Reconstruction with %d images and %d points", 111 | reconstruction.NumImages(), 112 | reconstruction.NumPoints3D()); 113 | 114 | std::vector image_ids; 115 | for (const auto& image_name : image_list) { 116 | const Image* image = reconstruction.FindImageWithName(image_name); 117 | if (image != nullptr) { 118 | image_ids.push_back(image->ImageId()); 119 | } else { 120 | LOG(WARNING) << "Cannot find image " << image_name; 121 | } 122 | } 123 | 124 | py::gil_scoped_release release; 125 | std::unique_ptr undistorter; 126 | if (output_type == "COLMAP") { 127 | undistorter.reset(new COLMAPUndistorter(undistort_camera_options, 128 | reconstruction, 129 | image_path, 130 | output_path, 131 | num_patch_match_src_images, 132 | copy_type, 133 | image_ids)); 134 | } else if (output_type == "PMVS") { 135 | undistorter.reset(new PMVSUndistorter( 136 | undistort_camera_options, reconstruction, image_path, output_path)); 137 | } else if (output_type == "CMP-MVS") { 138 | undistorter.reset(new CMPMVSUndistorter( 139 | undistort_camera_options, reconstruction, image_path, output_path)); 140 | } else { 141 | THROW_EXCEPTION( 142 | std::invalid_argument, 143 | std::string("Invalid `output_type` - supported values are ") + 144 | "{'COLMAP', 'PMVS', 'CMP-MVS'}."); 145 | } 146 | undistorter->Start(); 147 | PyWait(undistorter.get()); 148 | } 149 | 150 | void BindImages(py::module& m) { 151 | auto PyCameraMode = py::enum_(m, "CameraMode") 152 | .value("AUTO", CameraMode::AUTO) 153 | .value("SINGLE", CameraMode::SINGLE) 154 | .value("PER_FOLDER", CameraMode::PER_FOLDER) 155 | .value("PER_IMAGE", CameraMode::PER_IMAGE); 156 | AddStringToEnumConstructor(PyCameraMode); 157 | 158 | using IROpts = ImageReaderOptions; 159 | auto PyImageReaderOptions = 160 | py::class_(m, "ImageReaderOptions") 161 | .def(py::init<>()) 162 | .def_readwrite("camera_model", 163 | &IROpts::camera_model, 164 | "Name of the camera model.") 165 | .def_readwrite("mask_path", 166 | &IROpts::mask_path, 167 | "Optional root path to folder which contains image" 168 | "masks. For a given image, the corresponding mask" 169 | "must have the same sub-path below this root as the" 170 | "image has below image_path. The filename must be" 171 | "equal, aside from the added extension .png. " 172 | "For example, for an image image_path/abc/012.jpg," 173 | "the mask would be mask_path/abc/012.jpg.png. No" 174 | "features will be extracted in regions where the" 175 | "mask image is black (pixel intensity value 0 in" 176 | "grayscale).") 177 | .def_readwrite("existing_camera_id", 178 | &IROpts::existing_camera_id, 179 | "Whether to explicitly use an existing camera for " 180 | "all images. Note that in this case the specified " 181 | "camera model and parameters are ignored.") 182 | .def_readwrite("camera_params", 183 | &IROpts::camera_params, 184 | "Manual specification of camera parameters. If " 185 | "empty, camera parameters will be extracted from " 186 | "EXIF, i.e. principal point and focal length.") 187 | .def_readwrite( 188 | "default_focal_length_factor", 189 | &IROpts::default_focal_length_factor, 190 | "If camera parameters are not specified manually and the image " 191 | "does not have focal length EXIF information, the focal length " 192 | "is set to the value `default_focal_length_factor * max(width, " 193 | "height)`.") 194 | .def_readwrite( 195 | "camera_mask_path", 196 | &IROpts::camera_mask_path, 197 | "Optional path to an image file specifying a mask for all " 198 | "images. No features will be extracted in regions where the " 199 | "mask is black (pixel intensity value 0 in grayscale)"); 200 | MakeDataclass(PyImageReaderOptions); 201 | auto reader_options = PyImageReaderOptions().cast(); 202 | 203 | auto PyCopyType = py::enum_(m, "CopyType") 204 | .value("copy", CopyType::COPY) 205 | .value("soft-link", CopyType::SOFT_LINK) 206 | .value("hard-link", CopyType::HARD_LINK); 207 | AddStringToEnumConstructor(PyCopyType); 208 | 209 | using UDOpts = UndistortCameraOptions; 210 | auto PyUndistortCameraOptions = 211 | py::class_(m, "UndistortCameraOptions") 212 | .def(py::init<>()) 213 | .def_readwrite("blank_pixels", 214 | &UDOpts::blank_pixels, 215 | "The amount of blank pixels in the undistorted " 216 | "image in the range [0, 1].") 217 | .def_readwrite("min_scale", 218 | &UDOpts::min_scale, 219 | "Minimum scale change of camera used to satisfy the " 220 | "blank pixel constraint.") 221 | .def_readwrite("max_scale", 222 | &UDOpts::max_scale, 223 | "Maximum scale change of camera used to satisfy the " 224 | "blank pixel constraint.") 225 | .def_readwrite("max_image_size", 226 | &UDOpts::max_image_size, 227 | "Maximum image size in terms of width or height of " 228 | "the undistorted camera.") 229 | .def_readwrite("roi_min_x", &UDOpts::roi_min_x) 230 | .def_readwrite("roi_min_y", &UDOpts::roi_min_y) 231 | .def_readwrite("roi_max_x", &UDOpts::roi_max_x) 232 | .def_readwrite("roi_max_y", &UDOpts::roi_max_y); 233 | MakeDataclass(PyUndistortCameraOptions); 234 | auto undistort_options = PyUndistortCameraOptions().cast(); 235 | 236 | m.def("import_images", 237 | &ImportImages, 238 | "database_path"_a, 239 | "image_path"_a, 240 | "camera_mode"_a = CameraMode::AUTO, 241 | "image_list"_a = std::vector(), 242 | "options"_a = reader_options, 243 | "Import images into a database"); 244 | 245 | m.def("infer_camera_from_image", 246 | &InferCameraFromImage, 247 | "image_path"_a, 248 | "options"_a = reader_options, 249 | "Guess the camera parameters from the EXIF metadata"); 250 | 251 | m.def("undistort_images", 252 | &UndistortImages, 253 | "output_path"_a, 254 | "input_path"_a, 255 | "image_path"_a, 256 | "image_list"_a = std::vector(), 257 | "output_type"_a = "COLMAP", 258 | "copy_policy"_a = CopyType::COPY, 259 | "num_patch_match_src_images"_a = 20, 260 | "undistort_options"_a = undistort_options, 261 | "Undistort images"); 262 | } 263 | -------------------------------------------------------------------------------- /pycolmap/pipeline/match_features.h: -------------------------------------------------------------------------------- 1 | #include "colmap/controllers/feature_matching.h" 2 | #include "colmap/estimators/two_view_geometry.h" 3 | #include "colmap/exe/feature.h" 4 | #include "colmap/exe/sfm.h" 5 | #include "colmap/feature/sift.h" 6 | 7 | #include "pycolmap/helpers.h" 8 | #include "pycolmap/log_exceptions.h" 9 | #include "pycolmap/utils.h" 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | using namespace colmap; 19 | using namespace pybind11::literals; 20 | namespace py = pybind11; 21 | 22 | template MatcherFactory(const Opts&, 24 | const SiftMatchingOptions&, 25 | const TwoViewGeometryOptions&, 26 | const std::string&)> 27 | void MatchFeatures(const std::string& database_path, 28 | SiftMatchingOptions sift_options, 29 | const Opts& matching_options, 30 | const TwoViewGeometryOptions& verification_options, 31 | const Device device) { 32 | THROW_CHECK_FILE_EXISTS(database_path); 33 | try { 34 | py::cast(matching_options).attr("check").attr("__call__")(); 35 | } catch (py::error_already_set& ex) { 36 | // Allow pass if no check function defined. 37 | if (!ex.matches(PyExc_AttributeError)) { 38 | throw ex; 39 | } 40 | } 41 | 42 | sift_options.use_gpu = IsGPU(device); 43 | VerifyGPUParams(sift_options.use_gpu); 44 | py::gil_scoped_release release; 45 | std::unique_ptr matcher = MatcherFactory( 46 | matching_options, sift_options, verification_options, database_path); 47 | matcher->Start(); 48 | PyWait(matcher.get()); 49 | } 50 | 51 | void verify_matches(const std::string& database_path, 52 | const std::string& pairs_path, 53 | const TwoViewGeometryOptions& verification_options) { 54 | THROW_CHECK_FILE_EXISTS(database_path); 55 | THROW_CHECK_FILE_EXISTS(pairs_path); 56 | py::gil_scoped_release release; // verification is multi-threaded 57 | 58 | SiftMatchingOptions sift_options; 59 | sift_options.use_gpu = false; 60 | 61 | ImagePairsMatchingOptions matcher_options; 62 | matcher_options.match_list_path = pairs_path; 63 | 64 | std::unique_ptr matcher = CreateImagePairsFeatureMatcher( 65 | matcher_options, sift_options, verification_options, database_path); 66 | matcher->Start(); 67 | PyWait(matcher.get()); 68 | } 69 | 70 | void BindMatchFeatures(py::module& m) { 71 | using SMOpts = SiftMatchingOptions; 72 | auto PySiftMatchingOptions = 73 | py::class_(m, "SiftMatchingOptions") 74 | .def(py::init<>()) 75 | .def_readwrite("num_threads", &SMOpts::num_threads) 76 | .def_readwrite("gpu_index", 77 | &SMOpts::gpu_index, 78 | "Index of the GPU used for feature matching. For " 79 | "multi-GPU matching, " 80 | "you should separate multiple GPU indices by comma, " 81 | "e.g., \"0,1,2,3\".") 82 | .def_readwrite( 83 | "max_ratio", 84 | &SMOpts::max_ratio, 85 | "Maximum distance ratio between first and second best match.") 86 | .def_readwrite("max_distance", 87 | &SMOpts::max_distance, 88 | "Maximum distance to best match.") 89 | .def_readwrite("cross_check", 90 | &SMOpts::cross_check, 91 | "Whether to enable cross checking in matching.") 92 | .def_readwrite("max_num_matches", 93 | &SMOpts::max_num_matches, 94 | "Maximum number of matches.") 95 | .def_readwrite("guided_matching", 96 | &SMOpts::guided_matching, 97 | "Whether to perform guided matching, if geometric " 98 | "verification succeeds."); 99 | MakeDataclass(PySiftMatchingOptions); 100 | auto sift_matching_options = PySiftMatchingOptions().cast(); 101 | 102 | using EMOpts = ExhaustiveMatchingOptions; 103 | auto PyExhaustiveMatchingOptions = 104 | py::class_(m, "ExhaustiveMatchingOptions") 105 | .def(py::init<>()) 106 | .def_readwrite("block_size", &EMOpts::block_size); 107 | MakeDataclass(PyExhaustiveMatchingOptions); 108 | auto exhaustive_options = PyExhaustiveMatchingOptions().cast(); 109 | 110 | using SeqMOpts = SequentialMatchingOptions; 111 | auto PySequentialMatchingOptions = 112 | py::class_(m, "SequentialMatchingOptions") 113 | .def(py::init<>()) 114 | .def_readwrite("overlap", 115 | &SeqMOpts::overlap, 116 | "Number of overlapping image pairs.") 117 | .def_readwrite( 118 | "quadratic_overlap", 119 | &SeqMOpts::quadratic_overlap, 120 | "Whether to match images against their quadratic neighbors.") 121 | .def_readwrite("loop_detection", 122 | &SeqMOpts::loop_detection, 123 | "Loop detection is invoked every " 124 | "`loop_detection_period` images.") 125 | .def_readwrite("loop_detection_num_images", 126 | &SeqMOpts::loop_detection_num_images, 127 | "The number of images to retrieve in loop " 128 | "detection. This number should be significantly " 129 | "bigger than the sequential matching overlap.") 130 | .def_readwrite( 131 | "loop_detection_num_nearest_neighbors", 132 | &SeqMOpts::loop_detection_num_nearest_neighbors, 133 | "Number of nearest neighbors to retrieve per query feature.") 134 | .def_readwrite( 135 | "loop_detection_num_checks", 136 | &SeqMOpts::loop_detection_num_checks, 137 | "Number of nearest-neighbor checks to use in retrieval.") 138 | .def_readwrite( 139 | "loop_detection_num_images_after_verification", 140 | &SeqMOpts::loop_detection_num_images_after_verification, 141 | "How many images to return after spatial verification. Set to " 142 | "0 to turn off spatial verification.") 143 | .def_readwrite("loop_detection_max_num_features", 144 | &SeqMOpts::loop_detection_max_num_features, 145 | "The maximum number of features to use for indexing " 146 | "an image. If an image has more features, only the " 147 | "largest-scale features will be indexed.") 148 | .def_readwrite("vocab_tree_path", 149 | &SeqMOpts::vocab_tree_path, 150 | "Path to the vocabulary tree."); 151 | MakeDataclass(PySequentialMatchingOptions); 152 | auto sequential_options = PySequentialMatchingOptions().cast(); 153 | 154 | using SpMOpts = SpatialMatchingOptions; 155 | auto PySpatialMatchingOptions = 156 | py::class_(m, "SpatialMatchingOptions") 157 | .def(py::init<>()) 158 | .def_readwrite("is_gps", 159 | &SpMOpts::is_gps, 160 | "Whether the location priors in the database are " 161 | "GPS coordinates in the form of longitude and " 162 | "latitude coordinates in degrees.") 163 | .def_readwrite( 164 | "ignore_z", 165 | &SpMOpts::ignore_z, 166 | "Whether to ignore the Z-component of the location prior.") 167 | .def_readwrite("max_num_neighbors", 168 | &SpMOpts::max_num_neighbors, 169 | "The maximum number of nearest neighbors to match.") 170 | .def_readwrite("max_distance", 171 | &SpMOpts::max_distance, 172 | "The maximum distance between the query and nearest " 173 | "neighbor [meters]."); 174 | MakeDataclass(PySpatialMatchingOptions); 175 | auto spatial_options = PySpatialMatchingOptions().cast(); 176 | 177 | using VTMOpts = VocabTreeMatchingOptions; 178 | auto PyVocabTreeMatchingOptions = 179 | py::class_(m, "VocabTreeMatchingOptions") 180 | .def(py::init<>()) 181 | .def_readwrite("num_images", 182 | &VTMOpts::num_images, 183 | "Number of images to retrieve for each query image.") 184 | .def_readwrite( 185 | "num_nearest_neighbors", 186 | &VTMOpts::num_nearest_neighbors, 187 | "Number of nearest neighbors to retrieve per query feature.") 188 | .def_readwrite( 189 | "num_checks", 190 | &VTMOpts::num_checks, 191 | "Number of nearest-neighbor checks to use in retrieval.") 192 | .def_readwrite( 193 | "num_images_after_verification", 194 | &VTMOpts::num_images_after_verification, 195 | "How many images to return after spatial verification. Set to " 196 | "0 to turn off spatial verification.") 197 | .def_readwrite( 198 | "max_num_features", 199 | &VTMOpts::max_num_features, 200 | "The maximum number of features to use for indexing an image.") 201 | .def_readwrite("vocab_tree_path", 202 | &VTMOpts::vocab_tree_path, 203 | "Path to the vocabulary tree.") 204 | .def_readwrite( 205 | "match_list_path", 206 | &VTMOpts::match_list_path, 207 | "Optional path to file with specific image names to match.") 208 | .def("check", [](VTMOpts& self) { 209 | THROW_CHECK_MSG(!self.vocab_tree_path.empty(), 210 | "vocab_tree_path required."); 211 | THROW_CHECK_FILE_EXISTS(self.vocab_tree_path); 212 | }); 213 | MakeDataclass(PyVocabTreeMatchingOptions); 214 | auto vocabtree_options = PyVocabTreeMatchingOptions().cast(); 215 | 216 | auto verification_options = 217 | m.attr("TwoViewGeometryOptions")().cast(); 218 | 219 | m.def("match_exhaustive", 220 | &MatchFeatures, 221 | "database_path"_a, 222 | "sift_options"_a = sift_matching_options, 223 | "matching_options"_a = exhaustive_options, 224 | "verification_options"_a = verification_options, 225 | "device"_a = Device::AUTO, 226 | "Exhaustive feature matching"); 227 | 228 | m.def("match_sequential", 229 | &MatchFeatures, 230 | "database_path"_a, 231 | "sift_options"_a = sift_matching_options, 232 | "matching_options"_a = sequential_options, 233 | "verification_options"_a = verification_options, 234 | "device"_a = Device::AUTO, 235 | "Sequential feature matching"); 236 | 237 | m.def("match_spatial", 238 | &MatchFeatures, 239 | "database_path"_a, 240 | "sift_options"_a = sift_matching_options, 241 | "matching_options"_a = spatial_options, 242 | "verification_options"_a = verification_options, 243 | "device"_a = Device::AUTO, 244 | "Spatial feature matching"); 245 | 246 | m.def("match_vocabtree", 247 | &MatchFeatures, 248 | "database_path"_a, 249 | "sift_options"_a = sift_matching_options, 250 | "matching_options"_a = vocabtree_options, 251 | "verification_options"_a = verification_options, 252 | "device"_a = Device::AUTO, 253 | "Vocab tree feature matching"); 254 | 255 | m.def("verify_matches", 256 | &verify_matches, 257 | "database_path"_a, 258 | "pairs_path"_a, 259 | "options"_a = verification_options, 260 | "Run geometric verification of the matches"); 261 | } 262 | -------------------------------------------------------------------------------- /pycolmap/pipeline/meshing.h: -------------------------------------------------------------------------------- 1 | #include "colmap/mvs/meshing.h" 2 | 3 | #include "pycolmap/helpers.h" 4 | #include "pycolmap/log_exceptions.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace colmap; 11 | using namespace pybind11::literals; 12 | namespace py = pybind11; 13 | 14 | void BindMeshing(py::module& m) { 15 | using PoissonMOpts = mvs::PoissonMeshingOptions; 16 | auto PyPoissonMeshingOptions = 17 | py::class_(m, "PoissonMeshingOptions") 18 | .def(py::init<>()) 19 | .def_readwrite("point_weight", 20 | &PoissonMOpts::point_weight, 21 | "This floating point value specifies the importance " 22 | "that interpolation of" 23 | "the point samples is given in the formulation of the " 24 | "screened Poisson" 25 | "equation. The results of the original (unscreened) " 26 | "Poisson Reconstruction" 27 | "can be obtained by setting this value to 0.") 28 | .def_readwrite("depth", 29 | &PoissonMOpts::depth, 30 | "This integer is the maximum depth of the tree that " 31 | "will be used for surface" 32 | "reconstruction. Running at depth d corresponds to " 33 | "solving on a voxel grid" 34 | "whose resolution is no larger than 2^d x 2^d x 2^d. " 35 | "Note that since the" 36 | "reconstructor adapts the octree to the sampling " 37 | "density, the specified" 38 | "reconstruction depth is only an upper bound.") 39 | .def_readwrite("color", 40 | &PoissonMOpts::color, 41 | "If specified, the reconstruction code assumes that " 42 | "the input is equipped" 43 | "with colors and will extrapolate the color values to " 44 | "the vertices of the" 45 | "reconstructed mesh. The floating point value " 46 | "specifies the relative" 47 | "importance of finer color estimates over lower ones.") 48 | .def_readwrite("trim", 49 | &PoissonMOpts::trim, 50 | "This floating point values specifies the value for " 51 | "mesh trimming. The" 52 | "subset of the mesh with signal value less than the " 53 | "trim value is discarded.") 54 | .def_readwrite( 55 | "num_threads", 56 | &PoissonMOpts::num_threads, 57 | "The number of threads used for the Poisson reconstruction."); 58 | MakeDataclass(PyPoissonMeshingOptions); 59 | auto poisson_options = PyPoissonMeshingOptions().cast(); 60 | 61 | using DMOpts = mvs::DelaunayMeshingOptions; 62 | auto PyDelaunayMeshingOptions = 63 | py::class_(m, "DelaunayMeshingOptions") 64 | .def(py::init<>()) 65 | .def_readwrite("max_proj_dist", 66 | &DMOpts::max_proj_dist, 67 | "Unify input points into one cell in the Delaunay " 68 | "triangulation that fall" 69 | "within a reprojected radius of the given pixels.") 70 | .def_readwrite("max_depth_dist", 71 | &DMOpts::max_depth_dist, 72 | "Maximum relative depth difference between input " 73 | "point and a vertex of an" 74 | "existing cell in the Delaunay triangulation, " 75 | "otherwise a new vertex is" 76 | "created in the triangulation.") 77 | .def_readwrite("visibility_sigma", 78 | &DMOpts::visibility_sigma, 79 | "The standard deviation of wrt. the number of images " 80 | "seen by each point." 81 | "Increasing this value decreases the influence of " 82 | "points seen in few images.") 83 | .def_readwrite("distance_sigma_factor", 84 | &DMOpts::distance_sigma_factor, 85 | "The factor that is applied to the computed distance " 86 | "sigma, which is" 87 | "automatically computed as the 25th percentile of " 88 | "edge lengths. A higher" 89 | "value will increase the smoothness of the surface.") 90 | .def_readwrite( 91 | "quality_regularization", 92 | &DMOpts::quality_regularization, 93 | "A higher quality regularization leads to a smoother surface.") 94 | .def_readwrite("max_side_length_factor", 95 | &DMOpts::max_side_length_factor, 96 | "Filtering thresholds for outlier surface mesh faces. " 97 | "If the longest side of" 98 | "a mesh face (longest out of 3) exceeds the side " 99 | "lengths of all faces at a" 100 | "certain percentile by the given factor, then it is " 101 | "considered an outlier" 102 | "mesh face and discarded.") 103 | .def_readwrite("max_side_length_percentile", 104 | &DMOpts::max_side_length_percentile, 105 | "Filtering thresholds for outlier surface mesh faces. " 106 | "If the longest side of" 107 | "a mesh face (longest out of 3) exceeds the side " 108 | "lengths of all faces at a" 109 | "certain percentile by the given factor, then it is " 110 | "considered an outlier" 111 | "mesh face and discarded.") 112 | .def_readwrite("num_threads", 113 | &DMOpts::num_threads, 114 | "The number of threads to use for reconstruction. " 115 | "Default is all threads."); 116 | MakeDataclass(PyDelaunayMeshingOptions); 117 | auto delaunay_options = PyDelaunayMeshingOptions().cast(); 118 | 119 | m.def( 120 | "poisson_meshing", 121 | [](const std::string& input_path, 122 | const std::string& output_path, 123 | const PoissonMOpts& options) -> void { 124 | THROW_CHECK_HAS_FILE_EXTENSION(input_path, ".ply") 125 | THROW_CHECK_FILE_EXISTS(input_path); 126 | THROW_CHECK_HAS_FILE_EXTENSION(output_path, ".ply") 127 | THROW_CHECK_FILE_OPEN(output_path); 128 | PoissonMeshing(options, input_path, output_path); 129 | }, 130 | "input_path"_a, 131 | "output_path"_a, 132 | "options"_a = poisson_options, 133 | "Perform Poisson surface reconstruction and return true if successful."); 134 | 135 | #ifdef COLMAP_CGAL_ENABLED 136 | m.def( 137 | "sparse_delaunay_meshing", 138 | [](const std::string& input_path, 139 | const std::string& output_path, 140 | const DMOpts& options) -> void { 141 | THROW_CHECK_DIR_EXISTS(input_path); 142 | THROW_CHECK_HAS_FILE_EXTENSION(output_path, ".ply") 143 | THROW_CHECK_FILE_OPEN(output_path); 144 | mvs::SparseDelaunayMeshing(options, input_path, output_path); 145 | }, 146 | "input_path"_a, 147 | "output_path"_a, 148 | "options"_a = delaunay_options, 149 | "Delaunay meshing of sparse COLMAP reconstructions."); 150 | 151 | m.def( 152 | "dense_delaunay_meshing", 153 | [](const std::string& input_path, 154 | const std::string& output_path, 155 | const DMOpts& options) -> void { 156 | THROW_CHECK_DIR_EXISTS(input_path); 157 | THROW_CHECK_HAS_FILE_EXTENSION(output_path, ".bin") 158 | THROW_CHECK_FILE_OPEN(output_path); 159 | mvs::DenseDelaunayMeshing(options, input_path, output_path); 160 | }, 161 | "input_path"_a, 162 | "output_path"_a, 163 | "options"_a = delaunay_options, 164 | "Delaunay meshing of dense COLMAP reconstructions."); 165 | #endif 166 | }; 167 | -------------------------------------------------------------------------------- /pycolmap/pipeline/mvs.h: -------------------------------------------------------------------------------- 1 | #include "colmap/mvs/fusion.h" 2 | #include "colmap/scene/reconstruction.h" 3 | #include "colmap/util/misc.h" 4 | 5 | #ifdef COLMAP_CUDA_ENABLED 6 | #include "colmap/mvs/patch_match.h" 7 | #endif // COLMAP_CUDA_ENABLED 8 | 9 | #include "pycolmap/helpers.h" 10 | #include "pycolmap/log_exceptions.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace colmap; 18 | using namespace pybind11::literals; 19 | namespace py = pybind11; 20 | 21 | #ifdef COLMAP_CUDA_ENABLED 22 | void PatchMatchStereo(const std::string& workspace_path, 23 | std::string workspace_format, 24 | const std::string& pmvs_option_name, 25 | const mvs::PatchMatchOptions& options, 26 | const std::string& config_path) { 27 | THROW_CHECK_DIR_EXISTS(workspace_path); 28 | StringToLower(&workspace_format); 29 | THROW_CUSTOM_CHECK_MSG( 30 | (workspace_format == "colmap" || workspace_format == "pmvs"), 31 | std::invalid_argument, 32 | "Invalid `workspace_format` - supported values are " 33 | "'COLMAP' or 'PMVS'.") 34 | 35 | py::gil_scoped_release release; 36 | mvs::PatchMatchController controller( 37 | options, workspace_path, workspace_format, pmvs_option_name, config_path); 38 | controller.Start(); 39 | PyWait(&controller); 40 | } 41 | #endif // COLMAP_CUDA_ENABLED 42 | 43 | Reconstruction StereoFusion(const std::string& output_path, 44 | const std::string& workspace_path, 45 | std::string workspace_format, 46 | const std::string& pmvs_option_name, 47 | std::string input_type, 48 | const mvs::StereoFusionOptions& options) { 49 | THROW_CHECK_DIR_EXISTS(workspace_path); 50 | StringToLower(&workspace_format); 51 | THROW_CUSTOM_CHECK_MSG( 52 | (workspace_format == "colmap" || workspace_format == "pmvs"), 53 | std::invalid_argument, 54 | "Invalid `workspace_format` - supported values are " 55 | "'COLMAP' or 'PMVS'."); 56 | 57 | StringToLower(&input_type); 58 | THROW_CUSTOM_CHECK_MSG( 59 | (input_type == "photometric" || input_type == "geometric"), 60 | std::invalid_argument, 61 | "Invalid input type - supported values are 'photometric' and " 62 | "'geometric'."); 63 | 64 | py::gil_scoped_release release; 65 | mvs::StereoFusion fuser( 66 | options, workspace_path, workspace_format, pmvs_option_name, input_type); 67 | fuser.Start(); 68 | PyWait(&fuser); 69 | 70 | Reconstruction reconstruction; 71 | // read data from sparse reconstruction 72 | if (workspace_format == "colmap") { 73 | reconstruction.Read(JoinPaths(workspace_path, "sparse")); 74 | } 75 | 76 | // overwrite sparse point cloud with dense point cloud from fuser 77 | reconstruction.ImportPLY(fuser.GetFusedPoints()); 78 | 79 | if (ExistsDir(output_path)) { 80 | reconstruction.WriteBinary(output_path); 81 | } else { 82 | THROW_CHECK_HAS_FILE_EXTENSION(output_path, ".ply") 83 | THROW_CHECK_FILE_OPEN(output_path); 84 | WriteBinaryPlyPoints(output_path, fuser.GetFusedPoints()); 85 | mvs::WritePointsVisibility(output_path + ".vis", 86 | fuser.GetFusedPointsVisibility()); 87 | } 88 | 89 | return reconstruction; 90 | } 91 | 92 | void BindMVS(py::module& m) { 93 | #ifdef COLMAP_CUDA_ENABLED 94 | using PMOpts = mvs::PatchMatchOptions; 95 | auto PyPatchMatchOptions = 96 | py::class_(m, "PatchMatchOptions") 97 | .def(py::init<>()) 98 | .def_readwrite("max_image_size", 99 | &PMOpts::max_image_size, 100 | "Maximum image size in either dimension.") 101 | .def_readwrite( 102 | "gpu_index", 103 | &PMOpts::gpu_index, 104 | "Index of the GPU used for patch match. For multi-GPU usage, " 105 | "you should separate multiple GPU indices by comma, e.g., " 106 | "\"0,1,2,3\".") 107 | .def_readwrite("depth_min", &PMOpts::depth_min) 108 | .def_readwrite("depth_max", &PMOpts::depth_max) 109 | .def_readwrite( 110 | "window_radius", 111 | &PMOpts::window_radius, 112 | "Half window size to compute NCC photo-consistency cost.") 113 | .def_readwrite("window_step", 114 | &PMOpts::window_step, 115 | "Number of pixels to skip when computing NCC.") 116 | .def_readwrite("sigma_spatial", 117 | &PMOpts::sigma_spatial, 118 | "Spatial sigma for bilaterally weighted NCC.") 119 | .def_readwrite("sigma_color", 120 | &PMOpts::sigma_color, 121 | "Color sigma for bilaterally weighted NCC.") 122 | .def_readwrite( 123 | "num_samples", 124 | &PMOpts::num_samples, 125 | "Number of random samples to draw in Monte Carlo sampling.") 126 | .def_readwrite("ncc_sigma", 127 | &PMOpts::ncc_sigma, 128 | "Spread of the NCC likelihood function.") 129 | .def_readwrite("min_triangulation_angle", 130 | &PMOpts::min_triangulation_angle, 131 | "Minimum triangulation angle in degrees.") 132 | .def_readwrite("incident_angle_sigma", 133 | &PMOpts::incident_angle_sigma, 134 | "Spread of the incident angle likelihood function.") 135 | .def_readwrite("num_iterations", 136 | &PMOpts::num_iterations, 137 | "Number of coordinate descent iterations.") 138 | .def_readwrite("geom_consistency", 139 | &PMOpts::geom_consistency, 140 | "Whether to add a regularized geometric consistency " 141 | "term to the cost function. If true, the " 142 | "`depth_maps` and `normal_maps` must not be null.") 143 | .def_readwrite("geom_consistency_regularizer", 144 | &PMOpts::geom_consistency_regularizer, 145 | "The relative weight of the geometric consistency " 146 | "term w.r.t. to the photo-consistency term.") 147 | .def_readwrite("geom_consistency_max_cost", 148 | &PMOpts::geom_consistency_max_cost, 149 | "Maximum geometric consistency cost in terms of the " 150 | "forward-backward reprojection error in pixels.") 151 | .def_readwrite( 152 | "filter", &PMOpts::filter, "Whether to enable filtering.") 153 | .def_readwrite( 154 | "filter_min_ncc", 155 | &PMOpts::filter_min_ncc, 156 | "Minimum NCC coefficient for pixel to be photo-consistent.") 157 | .def_readwrite("filter_min_triangulation_angle", 158 | &PMOpts::filter_min_triangulation_angle, 159 | "Minimum triangulation angle to be stable.") 160 | .def_readwrite( 161 | "filter_min_num_consistent", 162 | &PMOpts::filter_min_num_consistent, 163 | "Minimum number of source images have to be consistent " 164 | "for pixel not to be filtered.") 165 | .def_readwrite( 166 | "filter_geom_consistency_max_cost", 167 | &PMOpts::filter_geom_consistency_max_cost, 168 | "Maximum forward-backward reprojection error for pixel " 169 | "to be geometrically consistent.") 170 | .def_readwrite("cache_size", 171 | &PMOpts::cache_size, 172 | "Cache size in gigabytes for patch match.") 173 | .def_readwrite( 174 | "allow_missing_files", 175 | &PMOpts::allow_missing_files, 176 | "Whether to tolerate missing images/maps in the problem setup") 177 | .def_readwrite("write_consistency_graph", 178 | &PMOpts::write_consistency_graph, 179 | "Whether to write the consistency graph."); 180 | MakeDataclass(PyPatchMatchOptions); 181 | auto patch_match_options = PyPatchMatchOptions().cast(); 182 | 183 | m.def("patch_match_stereo", 184 | &PatchMatchStereo, 185 | "workspace_path"_a, 186 | "workspace_format"_a = "COLMAP", 187 | "pmvs_option_name"_a = "option-all", 188 | "options"_a = patch_match_options, 189 | "config_path"_a = "", 190 | "Runs Patch-Match-Stereo (requires CUDA)"); 191 | #endif // COLMAP_CUDA_ENABLED 192 | 193 | using SFOpts = mvs::StereoFusionOptions; 194 | auto PyStereoFusionOptions = 195 | py::class_(m, "StereoFusionOptions") 196 | .def(py::init<>()) 197 | .def_readwrite("mask_path", 198 | &SFOpts::mask_path, 199 | "Path for PNG masks. Same format expected as " 200 | "ImageReaderOptions.") 201 | .def_readwrite("num_threads", 202 | &SFOpts::num_threads, 203 | "The number of threads to use during fusion.") 204 | .def_readwrite("max_image_size", 205 | &SFOpts::max_image_size, 206 | "Maximum image size in either dimension.") 207 | .def_readwrite("min_num_pixels", 208 | &SFOpts::min_num_pixels, 209 | "Minimum number of fused pixels to produce a point.") 210 | .def_readwrite( 211 | "max_num_pixels", 212 | &SFOpts::max_num_pixels, 213 | "Maximum number of pixels to fuse into a single point.") 214 | .def_readwrite("max_traversal_depth", 215 | &SFOpts::max_traversal_depth, 216 | "Maximum depth in consistency graph traversal.") 217 | .def_readwrite("max_reproj_error", 218 | &SFOpts::max_reproj_error, 219 | "Maximum relative difference between measured and " 220 | "projected pixel.") 221 | .def_readwrite("max_depth_error", 222 | &SFOpts::max_depth_error, 223 | "Maximum relative difference between measured and " 224 | "projected depth.") 225 | .def_readwrite("max_normal_error", 226 | &SFOpts::max_normal_error, 227 | "Maximum angular difference in degrees of normals " 228 | "of pixels to be fused.") 229 | .def_readwrite("check_num_images", 230 | &SFOpts::check_num_images, 231 | "Number of overlapping images to transitively check " 232 | "for fusing points.") 233 | .def_readwrite( 234 | "use_cache", 235 | &SFOpts::use_cache, 236 | "Flag indicating whether to use LRU cache or pre-load all data") 237 | .def_readwrite("cache_size", 238 | &SFOpts::cache_size, 239 | "Cache size in gigabytes for fusion.") 240 | .def_readwrite("bounding_box", 241 | &SFOpts::bounding_box, 242 | "Bounding box Tuple[min, max]"); 243 | MakeDataclass(PyStereoFusionOptions); 244 | auto stereo_fusion_options = PyStereoFusionOptions().cast(); 245 | 246 | m.def("stereo_fusion", 247 | &StereoFusion, 248 | "output_path"_a, 249 | "workspace_path"_a, 250 | "workspace_format"_a = "COLMAP", 251 | "pmvs_option_name"_a = "option-all", 252 | "input_type"_a = "geometric", 253 | "options"_a = stereo_fusion_options, 254 | "Stereo Fusion"); 255 | } 256 | -------------------------------------------------------------------------------- /pycolmap/pybind11_extension.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "colmap/util/types.h" 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | using namespace colmap; 16 | 17 | namespace PYBIND11_NAMESPACE { 18 | namespace detail { 19 | 20 | // Bind COLMAP's backport implementation of std::span. This copies the content 21 | // into a list. We could instead create a view with an Eigen::Map but the cast 22 | // should be explicit and cannot be automatic - likely not worth the added 23 | // logic. 24 | template 25 | struct type_caster> : list_caster, Type> {}; 26 | 27 | // Autocast os.PathLike to std::string 28 | // Adapted from pybind11/stl/filesystem.h 29 | template <> 30 | struct type_caster { 31 | public: 32 | PYBIND11_TYPE_CASTER(std::string, const_name(PYBIND11_STRING_NAME)); 33 | 34 | bool load(handle src, bool) { 35 | PyObject* buf = PyOS_FSPath(src.ptr()); 36 | if (!buf) { 37 | PyErr_Clear(); 38 | return false; 39 | } 40 | PyObject* native = nullptr; 41 | if (PyUnicode_FSConverter(buf, &native) != 0) { 42 | if (auto* c_str = PyBytes_AsString(native)) { 43 | value = c_str; 44 | } 45 | } 46 | Py_XDECREF(native); 47 | Py_DECREF(buf); 48 | if (PyErr_Occurred()) { 49 | PyErr_Clear(); 50 | return false; 51 | } 52 | return true; 53 | } 54 | 55 | static handle cast(const std::string& s, return_value_policy rvp, handle h) { 56 | return string_caster::cast(s, rvp, h); 57 | } 58 | }; 59 | 60 | // Autocast from numpy.ndarray to std::vector 61 | template 62 | struct type_caster>> { 63 | public: 64 | using MatrixType = 65 | typename Eigen::Matrix; 66 | using VectorType = typename Eigen::Matrix; 67 | using props = EigenProps; 68 | PYBIND11_TYPE_CASTER(std::vector, props::descriptor); 69 | 70 | bool load(handle src, bool) { 71 | const auto buf = array::ensure(src); 72 | if (!buf) { 73 | return false; 74 | } 75 | const buffer_info info = buf.request(); 76 | if (info.ndim != 2 || info.shape[1] != Size) { 77 | return false; 78 | } 79 | const size_t num_elements = info.shape[0]; 80 | value.resize(num_elements); 81 | const auto& mat = src.cast>(); 82 | Eigen::Map( 83 | reinterpret_cast(value.data()), num_elements, Size) = mat; 84 | return true; 85 | } 86 | 87 | static handle cast(const std::vector& vec, 88 | return_value_policy /* policy */, 89 | handle h) { 90 | Eigen::Map mat( 91 | reinterpret_cast(vec.data()), vec.size(), Size); 92 | return type_caster>::cast( 93 | mat, return_value_policy::copy, h); 94 | } 95 | }; 96 | 97 | } // namespace detail 98 | 99 | template 100 | class class_ext_ : public class_ { 101 | public: 102 | using Parent = class_; 103 | using Parent::class_; // inherit constructors 104 | using type = type_; 105 | 106 | template 107 | class_ext_& def_readwrite(const char* name, D C::*pm, const Extra&... extra) { 108 | static_assert( 109 | std::is_same::value || std::is_base_of::value, 110 | "def_readwrite() requires a class member (or base class member)"); 111 | cpp_function fget([pm](type&c) -> D& { return c.*pm; }, is_method(*this)), 112 | fset([pm](type&c, const D&value) { c.*pm = value; }, is_method(*this)); 113 | this->def_property( 114 | name, fget, fset, return_value_policy::reference_internal, extra...); 115 | return *this; 116 | } 117 | 118 | template 119 | class_ext_& def(Args&&... args) { 120 | Parent::def(std::forward(args)...); 121 | return *this; 122 | } 123 | 124 | template 125 | class_ext_& def_property(Args&&... args) { 126 | Parent::def_property(std::forward(args)...); 127 | return *this; 128 | } 129 | }; 130 | 131 | // Fix long-standing bug https://github.com/pybind/pybind11/issues/4529 132 | // TODO(sarlinpe): remove when https://github.com/pybind/pybind11/pull/4972 133 | // appears in the next release of pybind11. 134 | template , 136 | typename... Args> 137 | class_ bind_map_fix(handle scope, 138 | const std::string& name, 139 | Args&&... args) { 140 | using KeyType = typename Map::key_type; 141 | using MappedType = typename Map::mapped_type; 142 | using StrippedKeyType = detail::remove_cvref_t; 143 | using StrippedMappedType = detail::remove_cvref_t; 144 | using KeysView = detail::keys_view; 145 | using ValuesView = detail::values_view; 146 | using ItemsView = detail::items_view; 147 | using Class_ = class_; 148 | 149 | // If either type is a non-module-local bound type then make the map binding 150 | // non-local as well; otherwise (e.g. both types are either module-local or 151 | // converting) the map will be module-local. 152 | auto* tinfo = detail::get_type_info(typeid(MappedType)); 153 | bool local = !tinfo || tinfo->module_local; 154 | if (local) { 155 | tinfo = detail::get_type_info(typeid(KeyType)); 156 | local = !tinfo || tinfo->module_local; 157 | } 158 | 159 | Class_ cl(scope, 160 | name.c_str(), 161 | pybind11::module_local(local), 162 | std::forward(args)...); 163 | std::string key_type_name(detail::type_info_description(typeid(KeyType))); 164 | std::string mapped_type_name( 165 | detail::type_info_description(typeid(MappedType))); 166 | 167 | // Wrap KeysView[KeyType] if it wasn't already wrapped 168 | if (!detail::get_type_info(typeid(KeysView))) { 169 | class_ keys_view(scope, 170 | ("KeysView[" + key_type_name + "]").c_str(), 171 | pybind11::module_local(local)); 172 | keys_view.def("__len__", &KeysView::len); 173 | keys_view.def("__iter__", 174 | &KeysView::iter, 175 | keep_alive<0, 1>() /* Essential: keep view alive while 176 | iterator exists */ 177 | ); 178 | keys_view.def( 179 | "__contains__", 180 | static_cast(&KeysView::contains)); 181 | // Fallback for when the object is not of the key type 182 | keys_view.def( 183 | "__contains__", 184 | static_cast(&KeysView::contains)); 185 | } 186 | // Similarly for ValuesView: 187 | if (!detail::get_type_info(typeid(ValuesView))) { 188 | class_ values_view( 189 | scope, 190 | ("ValuesView[" + mapped_type_name + "]").c_str(), 191 | pybind11::module_local(local)); 192 | values_view.def("__len__", &ValuesView::len); 193 | values_view.def("__iter__", 194 | &ValuesView::iter, 195 | keep_alive<0, 1>() /* Essential: keep view alive while 196 | iterator exists */ 197 | ); 198 | } 199 | // Similarly for ItemsView: 200 | if (!detail::get_type_info(typeid(ItemsView))) { 201 | class_ items_view(scope, 202 | ("ItemsView[" + key_type_name + ", ") 203 | .append(mapped_type_name + "]") 204 | .c_str(), 205 | pybind11::module_local(local)); 206 | items_view.def("__len__", &ItemsView::len); 207 | items_view.def("__iter__", 208 | &ItemsView::iter, 209 | keep_alive<0, 1>() /* Essential: keep view alive while 210 | iterator exists */ 211 | ); 212 | } 213 | 214 | cl.def(init<>()); 215 | 216 | // Register stream insertion operator (if possible) 217 | detail::map_if_insertion_operator(cl, name); 218 | 219 | cl.def( 220 | "__bool__", 221 | [](const Map& m) -> bool { return !m.empty(); }, 222 | "Check whether the map is nonempty"); 223 | 224 | cl.def( 225 | "__iter__", 226 | [](Map& m) { return make_key_iterator(m.begin(), m.end()); }, 227 | keep_alive<0, 1>() /* Essential: keep map alive while iterator exists */ 228 | ); 229 | 230 | cl.def( 231 | "keys", 232 | [](Map& m) { 233 | return std::unique_ptr( 234 | new detail::KeysViewImpl(m)); 235 | }, 236 | keep_alive<0, 1>() /* Essential: keep map alive while view exists */ 237 | ); 238 | 239 | cl.def( 240 | "values", 241 | [](Map& m) { 242 | return std::unique_ptr( 243 | new detail::ValuesViewImpl(m)); 244 | }, 245 | keep_alive<0, 1>() /* Essential: keep map alive while view exists */ 246 | ); 247 | 248 | cl.def( 249 | "items", 250 | [](Map& m) { 251 | return std::unique_ptr( 252 | new detail::ItemsViewImpl(m)); 253 | }, 254 | keep_alive<0, 1>() /* Essential: keep map alive while view exists */ 255 | ); 256 | 257 | cl.def( 258 | "__getitem__", 259 | [](Map& m, const KeyType& k) -> MappedType& { 260 | auto it = m.find(k); 261 | if (it == m.end()) { 262 | throw key_error(); 263 | } 264 | return it->second; 265 | }, 266 | return_value_policy::reference_internal // ref + keepalive 267 | ); 268 | 269 | cl.def("__contains__", [](Map& m, const KeyType& k) -> bool { 270 | auto it = m.find(k); 271 | if (it == m.end()) { 272 | return false; 273 | } 274 | return true; 275 | }); 276 | // Fallback for when the object is not of the key type 277 | cl.def("__contains__", [](Map&, const object&) -> bool { return false; }); 278 | 279 | // Assignment provided only if the type is copyable 280 | detail::map_assignment(cl); 281 | 282 | cl.def("__delitem__", [](Map& m, const KeyType& k) { 283 | auto it = m.find(k); 284 | if (it == m.end()) { 285 | throw key_error(); 286 | } 287 | m.erase(it); 288 | }); 289 | 290 | // Always use a lambda in case of `using` declaration 291 | cl.def("__len__", [](const Map& m) { return m.size(); }); 292 | 293 | return cl; 294 | } 295 | } // namespace PYBIND11_NAMESPACE 296 | -------------------------------------------------------------------------------- /pycolmap/scene/bindings.h: -------------------------------------------------------------------------------- 1 | #include "pycolmap/scene/camera.h" 2 | #include "pycolmap/scene/correspondence_graph.h" 3 | #include "pycolmap/scene/database.h" 4 | #include "pycolmap/scene/image.h" 5 | #include "pycolmap/scene/point2D.h" 6 | #include "pycolmap/scene/point3D.h" 7 | #include "pycolmap/scene/reconstruction.h" 8 | #include "pycolmap/scene/track.h" 9 | 10 | #include 11 | 12 | namespace py = pybind11; 13 | 14 | void BindScene(py::module& m) { 15 | BindImage(m); 16 | BindCamera(m); 17 | BindPoint2D(m); 18 | BindTrack(m); 19 | BindPoint3D(m); 20 | BindCorrespondenceGraph(m); 21 | BindReconstruction(m); 22 | BindDatabase(m); 23 | } 24 | -------------------------------------------------------------------------------- /pycolmap/scene/camera.h: -------------------------------------------------------------------------------- 1 | #include "colmap/scene/camera.h" 2 | #include "colmap/sensor/models.h" 3 | #include "colmap/util/misc.h" 4 | #include "colmap/util/types.h" 5 | 6 | #include "pycolmap/helpers.h" 7 | #include "pycolmap/log_exceptions.h" 8 | #include "pycolmap/pybind11_extension.h" 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | using namespace colmap; 19 | using namespace pybind11::literals; 20 | namespace py = pybind11; 21 | 22 | using CameraMap = std::unordered_map; 23 | PYBIND11_MAKE_OPAQUE(CameraMap); 24 | 25 | std::string PrintCamera(const Camera& camera) { 26 | const bool valid_model = ExistsCameraModelWithId(camera.model_id); 27 | const std::string params_info = valid_model ? camera.ParamsInfo() : "?"; 28 | const std::string model_name = valid_model ? camera.ModelName() : "Invalid"; 29 | std::stringstream ss; 30 | ss << "Camera(camera_id=" 31 | << (camera.camera_id != kInvalidCameraId ? std::to_string(camera.camera_id) 32 | : "Invalid") 33 | << ", model=" << model_name << ", width=" << camera.width 34 | << ", height=" << camera.height << ", params=[" << camera.ParamsToString() 35 | << "] (" << params_info << "))"; 36 | return ss.str(); 37 | } 38 | 39 | void BindCamera(py::module& m) { 40 | py::enum_ PyCameraModelId(m, "CameraModelId"); 41 | PyCameraModelId.value("INVALID", CameraModelId::kInvalid); 42 | #define CAMERA_MODEL_CASE(CameraModel) \ 43 | PyCameraModelId.value(CameraModel::model_name.c_str(), CameraModel::model_id); 44 | 45 | CAMERA_MODEL_CASES 46 | 47 | #undef CAMERA_MODEL_CASE 48 | AddStringToEnumConstructor(PyCameraModelId); 49 | py::implicitly_convertible(); 50 | 51 | py::bind_map(m, "MapCameraIdToCamera") 52 | .def("__repr__", [](const CameraMap& self) { 53 | std::stringstream ss; 54 | ss << "{"; 55 | bool is_first = true; 56 | for (const auto& pair : self) { 57 | if (!is_first) { 58 | ss << ",\n "; 59 | } 60 | is_first = false; 61 | ss << pair.first << ": " << PrintCamera(pair.second); 62 | } 63 | ss << "}"; 64 | return ss.str(); 65 | }); 66 | 67 | py::class_> PyCamera(m, "Camera"); 68 | PyCamera.def(py::init<>()) 69 | .def_static("create", 70 | &Camera::CreateFromModelId, 71 | "camera_id"_a, 72 | "model"_a, 73 | "focal_length"_a, 74 | "width"_a, 75 | "height"_a) 76 | .def_readwrite( 77 | "camera_id", &Camera::camera_id, "Unique identifier of the camera.") 78 | .def_readwrite("model", &Camera::model_id, "Camera model.") 79 | .def_readwrite("width", &Camera::width, "Width of camera sensor.") 80 | .def_readwrite("height", &Camera::height, "Height of camera sensor.") 81 | .def("mean_focal_length", &Camera::MeanFocalLength) 82 | .def_property( 83 | "focal_length", &Camera::FocalLength, &Camera::SetFocalLength) 84 | .def_property( 85 | "focal_length_x", &Camera::FocalLengthX, &Camera::SetFocalLengthX) 86 | .def_property( 87 | "focal_length_y", &Camera::FocalLengthY, &Camera::SetFocalLengthY) 88 | .def_readwrite("has_prior_focal_length", &Camera::has_prior_focal_length) 89 | .def_property("principal_point_x", 90 | &Camera::PrincipalPointX, 91 | &Camera::SetPrincipalPointX) 92 | .def_property("principal_point_y", 93 | &Camera::PrincipalPointY, 94 | &Camera::SetPrincipalPointY) 95 | .def("focal_length_idxs", 96 | &Camera::FocalLengthIdxs, 97 | "Indices of focal length parameters in params property.") 98 | .def("principal_point_idxs", 99 | &Camera::PrincipalPointIdxs, 100 | "Indices of principal point parameters in params property.") 101 | .def("extra_params_idxs", 102 | &Camera::ExtraParamsIdxs, 103 | "Indices of extra parameters in params property.") 104 | .def("calibration_matrix", 105 | &Camera::CalibrationMatrix, 106 | "Compute calibration matrix from params.") 107 | .def_property_readonly( 108 | "params_info", 109 | &Camera::ParamsInfo, 110 | "Get human-readable information about the parameter vector " 111 | "ordering.") 112 | .def_property( 113 | "params", 114 | [](Camera& self) { 115 | // Return a view (via a numpy array) instead of a copy. 116 | return Eigen::Map(self.params.data(), 117 | self.params.size()); 118 | }, 119 | [](Camera& self, const std::vector& params) { 120 | self.params = params; 121 | }, 122 | "Camera parameters.") 123 | .def("params_to_string", 124 | &Camera::ParamsToString, 125 | "Concatenate parameters as comma-separated list.") 126 | .def("set_params_from_string", 127 | &Camera::SetParamsFromString, 128 | "Set camera parameters from comma-separated list.") 129 | .def("verify_params", 130 | &Camera::VerifyParams, 131 | "Check whether parameters are valid, i.e. the parameter vector has" 132 | "\nthe correct dimensions that match the specified camera model.") 133 | .def("has_bogus_params", 134 | &Camera::HasBogusParams, 135 | "Check whether camera has bogus parameters.") 136 | .def("cam_from_img", 137 | &Camera::CamFromImg, 138 | "Project point in image plane to world / infinity.") 139 | .def( 140 | "cam_from_img", 141 | [](const Camera& self, 142 | const py::EigenDRef& image_points) { 143 | std::vector world_points(image_points.rows()); 144 | for (size_t idx = 0; idx < image_points.rows(); ++idx) { 145 | world_points[idx] = self.CamFromImg(image_points.row(idx)); 146 | } 147 | return world_points; 148 | }, 149 | "Project list of points in image plane to world / infinity.") 150 | .def( 151 | "cam_from_img", 152 | [](const Camera& self, const std::vector& image_points) { 153 | std::vector world_points(image_points.size()); 154 | for (size_t idx = 0; idx < image_points.size(); ++idx) { 155 | world_points[idx] = self.CamFromImg(image_points[idx].xy); 156 | } 157 | return world_points; 158 | }, 159 | "Project list of points in image plane to world / infinity.") 160 | .def("cam_from_img_threshold", 161 | &Camera::CamFromImgThreshold, 162 | "Convert pixel threshold in image plane to world space.") 163 | .def("img_from_cam", 164 | &Camera::ImgFromCam, 165 | "Project point from world / infinity to image plane.") 166 | .def( 167 | "img_from_cam", 168 | [](const Camera& self, 169 | const py::EigenDRef& world_points) { 170 | std::vector image_points(world_points.rows()); 171 | for (size_t idx = 0; idx < world_points.rows(); ++idx) { 172 | image_points[idx] = self.ImgFromCam(world_points.row(idx)); 173 | } 174 | return image_points; 175 | }, 176 | "Project list of points from world / infinity to image plane.") 177 | .def( 178 | "img_from_cam", 179 | [](const Camera& self, 180 | const py::EigenDRef& world_points) { 181 | return py::cast(self).attr("img_from_cam")( 182 | world_points.rowwise().hnormalized()); 183 | }, 184 | "Project list of points from world / infinity to image plane.") 185 | .def( 186 | "img_from_cam", 187 | [](const Camera& self, const std::vector& world_points) { 188 | std::vector image_points(world_points.size()); 189 | for (size_t idx = 0; idx < world_points.size(); ++idx) { 190 | image_points[idx] = self.ImgFromCam(world_points[idx].xy); 191 | } 192 | return image_points; 193 | }, 194 | "Project list of points from world / infinity to image plane.") 195 | .def("rescale", 196 | py::overload_cast(&Camera::Rescale), 197 | "Rescale camera dimensions to (width_height) and accordingly the " 198 | "focal length and\n" 199 | "and the principal point.") 200 | .def("rescale", 201 | py::overload_cast(&Camera::Rescale), 202 | "Rescale camera dimensions by given factor and accordingly the " 203 | "focal length and\n" 204 | "and the principal point.") 205 | .def("__repr__", &PrintCamera); 206 | MakeDataclass(PyCamera, 207 | {"camera_id", 208 | "model", 209 | "width", 210 | "height", 211 | "params", 212 | "has_prior_focal_length"}); 213 | } 214 | -------------------------------------------------------------------------------- /pycolmap/scene/correspondence_graph.h: -------------------------------------------------------------------------------- 1 | #include "colmap/feature/types.h" 2 | #include "colmap/scene/correspondence_graph.h" 3 | #include "colmap/util/types.h" 4 | 5 | #include "pycolmap/log_exceptions.h" 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | using namespace colmap; 16 | using namespace pybind11::literals; 17 | namespace py = pybind11; 18 | 19 | void BindCorrespondenceGraph(py::module& m) { 20 | py::class_>( 22 | m, "Correspondence") 23 | .def(py::init<>()) 24 | .def(py::init()) 25 | .def_readwrite("image_id", &CorrespondenceGraph::Correspondence::image_id) 26 | .def_readwrite("point2D_idx", 27 | &CorrespondenceGraph::Correspondence::point2D_idx) 28 | .def("__copy__", 29 | [](const CorrespondenceGraph::Correspondence& self) { 30 | return CorrespondenceGraph::Correspondence(self); 31 | }) 32 | .def( 33 | "__deepcopy__", 34 | [](const CorrespondenceGraph::Correspondence& self, const py::dict&) { 35 | return CorrespondenceGraph::Correspondence(self); 36 | }) 37 | .def("__repr__", [](const CorrespondenceGraph::Correspondence& self) { 38 | return "Correspondence(image_id=" + std::to_string(self.image_id) + 39 | ", point2D_idx=" + std::to_string(self.point2D_idx) + ")"; 40 | }); 41 | 42 | py::class_>( 43 | m, "CorrespondenceGraph") 44 | .def(py::init<>()) 45 | .def("num_images", &CorrespondenceGraph::NumImages) 46 | .def("num_image_pairs", &CorrespondenceGraph::NumImagePairs) 47 | .def("exists_image", &CorrespondenceGraph::ExistsImage) 48 | .def("num_observations_for_image", 49 | &CorrespondenceGraph::NumObservationsForImage) 50 | .def("num_correspondences_for_image", 51 | &CorrespondenceGraph::NumCorrespondencesForImage) 52 | .def("num_correspondences_between_images", 53 | [](const CorrespondenceGraph& self, 54 | const image_t image_id1, 55 | const image_t image_id2) { 56 | return self.NumCorrespondencesBetweenImages(image_id1, image_id2); 57 | }) 58 | .def("finalize", &CorrespondenceGraph::Finalize) 59 | .def("add_image", &CorrespondenceGraph::AddImage) 60 | .def( 61 | "add_correspondences", 62 | [](CorrespondenceGraph& self, 63 | const image_t image_id1, 64 | const image_t image_id2, 65 | const Eigen::Ref>& 66 | corrs) { 67 | FeatureMatches matches; 68 | matches.reserve(corrs.rows()); 69 | for (Eigen::Index idx = 0; idx < corrs.rows(); ++idx) { 70 | matches.push_back(FeatureMatch(corrs(idx, 0), corrs(idx, 1))); 71 | } 72 | self.AddCorrespondences(image_id1, image_id2, matches); 73 | }) 74 | .def("extract_correspondences", 75 | &CorrespondenceGraph::ExtractCorrespondences) 76 | .def("extract_transitive_correspondences", 77 | &CorrespondenceGraph::ExtractTransitiveCorrespondences) 78 | .def("find_correspondences_between_images", 79 | [](const CorrespondenceGraph& self, 80 | const image_t image_id1, 81 | const image_t image_id2) { 82 | const FeatureMatches matches = 83 | self.FindCorrespondencesBetweenImages(image_id1, image_id2); 84 | Eigen::Matrix corrs( 85 | matches.size(), 2); 86 | for (size_t idx = 0; idx < matches.size(); ++idx) { 87 | corrs(idx, 0) = matches[idx].point2D_idx1; 88 | corrs(idx, 1) = matches[idx].point2D_idx2; 89 | } 90 | return corrs; 91 | }) 92 | .def("has_correspondences", &CorrespondenceGraph::HasCorrespondences) 93 | .def("is_two_view_observation", 94 | &CorrespondenceGraph::IsTwoViewObservation) 95 | .def("__copy__", 96 | [](const CorrespondenceGraph& self) { 97 | return CorrespondenceGraph(self); 98 | }) 99 | .def("__deepcopy__", 100 | [](const CorrespondenceGraph& self, const py::dict&) { 101 | return CorrespondenceGraph(self); 102 | }) 103 | .def("__repr__", [](const CorrespondenceGraph& self) { 104 | std::stringstream ss; 105 | ss << "CorrespondenceGraph(num_images=" << self.NumImages() 106 | << ", num_image_pairs=" << self.NumImagePairs() << ")"; 107 | return ss.str(); 108 | }); 109 | } 110 | -------------------------------------------------------------------------------- /pycolmap/scene/database.h: -------------------------------------------------------------------------------- 1 | #include "colmap/scene/database.h" 2 | 3 | #include 4 | 5 | using namespace colmap; 6 | using namespace pybind11::literals; 7 | namespace py = pybind11; 8 | 9 | void BindDatabase(py::module& m) { 10 | py::class_ PyDatabase(m, "Database"); 11 | PyDatabase.def(py::init(), "path"_a) 12 | .def("open", &Database::Open, "path"_a) 13 | .def("close", &Database::Close) 14 | .def_property_readonly("num_cameras", &Database::NumCameras) 15 | .def_property_readonly("num_images", &Database::NumImages) 16 | .def_property_readonly("num_keypoints", &Database::NumKeypoints) 17 | .def_property_readonly("num_keypoints_for_image", 18 | &Database::NumKeypointsForImage) 19 | .def_property_readonly("num_descriptors", &Database::NumDescriptors) 20 | .def_property_readonly("num_descriptors_for_image", 21 | &Database::NumDescriptorsForImage) 22 | .def_property_readonly("num_matches", &Database::NumMatches) 23 | .def_property_readonly("num_inlier_matches", &Database::NumInlierMatches) 24 | .def_property_readonly("num_matched_image_pairs", 25 | &Database::NumMatchedImagePairs) 26 | .def_property_readonly("num_verified_image_pairs", 27 | &Database::NumVerifiedImagePairs) 28 | .def("image_pair_to_pair_id", &Database::ImagePairToPairId) 29 | .def("pair_id_to_image_pair", &Database::PairIdToImagePair) 30 | .def("read_camera", &Database::ReadCamera) 31 | .def("read_all_cameras", &Database::ReadAllCameras) 32 | .def("read_image", &Database::ReadImage) 33 | .def("read_image_with_name", &Database::ReadImageWithName) 34 | .def("read_all_images", &Database::ReadAllImages) 35 | // ReadKeypoints 36 | // ReadDescriptors 37 | // ReadMatches 38 | // ReadAllMatches 39 | .def("read_two_view_geometry", &Database::ReadTwoViewGeometry) 40 | // ReadTwoViewGeometries 41 | // ReadTwoViewGeometryNumInliers 42 | .def("write_camera", &Database::WriteCamera) 43 | .def("write_image", &Database::WriteImage); 44 | 45 | py::class_(m, "DatabaseTransaction") 46 | .def(py::init(), "database"_a); 47 | } 48 | -------------------------------------------------------------------------------- /pycolmap/scene/image.h: -------------------------------------------------------------------------------- 1 | #include "colmap/geometry/rigid3.h" 2 | #include "colmap/scene/image.h" 3 | #include "colmap/util/misc.h" 4 | #include "colmap/util/types.h" 5 | 6 | #include "pycolmap/helpers.h" 7 | #include "pycolmap/log_exceptions.h" 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace colmap; 18 | using namespace pybind11::literals; 19 | namespace py = pybind11; 20 | 21 | using ImageMap = std::unordered_map; 22 | PYBIND11_MAKE_OPAQUE(ImageMap); 23 | 24 | std::string PrintImage(const Image& image) { 25 | std::stringstream ss; 26 | ss << "Image(image_id=" 27 | << (image.ImageId() != kInvalidImageId ? std::to_string(image.ImageId()) 28 | : "Invalid") 29 | << ", camera_id=" 30 | << (image.HasCamera() ? std::to_string(image.CameraId()) : "Invalid") 31 | << ", name=\"" << image.Name() << "\"" 32 | << ", triangulated=" << image.NumPoints3D() << "/" << image.NumPoints2D() 33 | << ")"; 34 | return ss.str(); 35 | } 36 | 37 | template 38 | std::shared_ptr MakeImage(const std::string& name, 39 | const std::vector& points2D, 40 | const Rigid3d& cam_from_world, 41 | size_t camera_id, 42 | image_t image_id) { 43 | auto image = std::make_shared(); 44 | image->SetName(name); 45 | image->SetPoints2D(points2D); 46 | image->CamFromWorld() = cam_from_world; 47 | if (camera_id != kInvalidCameraId) { 48 | image->SetCameraId(camera_id); 49 | } 50 | image->SetImageId(image_id); 51 | return image; 52 | } 53 | 54 | void BindImage(py::module& m) { 55 | py::bind_map(m, "MapImageIdToImage") 56 | .def("__repr__", [](const ImageMap& self) { 57 | std::stringstream ss; 58 | ss << "{"; 59 | bool is_first = true; 60 | for (const auto& pair : self) { 61 | if (!is_first) { 62 | ss << ",\n "; 63 | } 64 | is_first = false; 65 | ss << pair.first << ": " << PrintImage(pair.second); 66 | } 67 | ss << "}"; 68 | return ss.str(); 69 | }); 70 | 71 | py::class_> PyImage(m, "Image"); 72 | PyImage.def(py::init<>()) 73 | .def(py::init(&MakeImage), 74 | "name"_a = "", 75 | "points2D"_a = std::vector(), 76 | "cam_from_world"_a = Rigid3d(), 77 | "camera_id"_a = kInvalidCameraId, 78 | "id"_a = kInvalidImageId) 79 | .def(py::init(&MakeImage), 80 | "name"_a = "", 81 | "keypoints"_a = std::vector(), 82 | "cam_from_world"_a = Rigid3d(), 83 | "camera_id"_a = kInvalidCameraId, 84 | "id"_a = kInvalidImageId) 85 | .def_property("image_id", 86 | &Image::ImageId, 87 | &Image::SetImageId, 88 | "Unique identifier of image.") 89 | .def_property( 90 | "camera_id", 91 | &Image::CameraId, 92 | [](Image& self, const camera_t camera_id) { 93 | THROW_CHECK_NE(camera_id, kInvalidCameraId); 94 | self.SetCameraId(camera_id); 95 | }, 96 | "Unique identifier of the camera.") 97 | .def_property("name", 98 | py::overload_cast<>(&Image::Name), 99 | &Image::SetName, 100 | "Name of the image.") 101 | .def_property( 102 | "cam_from_world", 103 | py::overload_cast<>(&Image::CamFromWorld), 104 | [](Image& self, const Rigid3d& cam_from_world) { 105 | self.CamFromWorld() = cam_from_world; 106 | }, 107 | "The pose of the image, defined as the transformation from world to " 108 | "camera space.") 109 | .def_property( 110 | "cam_from_world_prior", 111 | py::overload_cast<>(&Image::CamFromWorldPrior), 112 | [](Image& self, const Rigid3d& cam_from_world) { 113 | self.CamFromWorldPrior() = cam_from_world; 114 | }, 115 | "The pose prior of the image, e.g. extracted from EXIF tags.") 116 | .def_property( 117 | "points2D", 118 | py::overload_cast<>(&Image::Points2D), 119 | [](Image& self, const std::vector& points2D) { 120 | THROW_CUSTOM_CHECK(!points2D.empty(), std::invalid_argument); 121 | self.SetPoints2D(points2D); 122 | }, 123 | "Array of Points2D (=keypoints).") 124 | .def( 125 | "set_point3D_for_point2D", 126 | [](Image& self, 127 | const point2D_t point2D_idx, 128 | const point3D_t point3D_id) { 129 | THROW_CHECK_NE(point3D_id, kInvalidPoint3DId); 130 | self.SetPoint3DForPoint2D(point2D_idx, point3D_id); 131 | }, 132 | "Set the point as triangulated, i.e. it is part of a 3D point track.") 133 | .def("reset_point3D_for_point2D", 134 | &Image::ResetPoint3DForPoint2D, 135 | "Set the point as not triangulated, i.e. it is not part of a 3D " 136 | "point track") 137 | .def("is_point3D_visible", 138 | &Image::IsPoint3DVisible, 139 | "Check whether an image point has a correspondence to an image " 140 | "point in\n" 141 | "another image that has a 3D point.") 142 | .def("has_point3D", 143 | &Image::HasPoint3D, 144 | "Check whether one of the image points is part of the 3D point " 145 | "track.") 146 | .def("increment_correspondence_has_point3D", 147 | &Image::IncrementCorrespondenceHasPoint3D, 148 | "Indicate that another image has a point that is triangulated and " 149 | "has\n" 150 | "a correspondence to this image point. Note that this must only be " 151 | "called\n" 152 | "after calling `SetUp`.") 153 | .def("decrement_correspondence_has_point3D", 154 | &Image::DecrementCorrespondenceHasPoint3D, 155 | "Indicate that another image has a point that is not triangulated " 156 | "any more\n" 157 | "and has a correspondence to this image point. This assumes that\n" 158 | "`IncrementCorrespondenceHasPoint3D` was called for the same image " 159 | "point\n" 160 | "and correspondence before. Note that this must only be called\n" 161 | "after calling `SetUp`.") 162 | .def("projection_center", 163 | &Image::ProjectionCenter, 164 | "Extract the projection center in world space.") 165 | .def("viewing_direction", 166 | &Image::ViewingDirection, 167 | "Extract the viewing direction of the image.") 168 | .def( 169 | "set_up", 170 | [](Image& self, const struct Camera& camera) { 171 | THROW_CHECK_EQ(self.CameraId(), camera.camera_id); 172 | self.SetUp(camera); 173 | }, 174 | "Setup the image and necessary internal data structures before being " 175 | "used in reconstruction.") 176 | .def("has_camera", 177 | &Image::HasCamera, 178 | "Check whether identifier of camera has been set.") 179 | .def_property("registered", 180 | &Image::IsRegistered, 181 | &Image::SetRegistered, 182 | "Whether image is registered in the reconstruction.") 183 | .def("num_points2D", 184 | &Image::NumPoints2D, 185 | "Get the number of image points (keypoints).") 186 | .def_property_readonly( 187 | "num_points3D", 188 | &Image::NumPoints3D, 189 | "Get the number of triangulations, i.e. the number of points that\n" 190 | "are part of a 3D point track.") 191 | .def_property( 192 | "num_observations", 193 | &Image::NumObservations, 194 | &Image::SetNumObservations, 195 | "Number of observations, i.e. the number of image points that\n" 196 | "have at least one correspondence to another image.") 197 | .def_property("num_correspondences", 198 | &Image::NumCorrespondences, 199 | &Image::SetNumCorrespondences, 200 | "Number of correspondences for all image points.") 201 | .def("num_visible_points3D", 202 | &Image::NumVisiblePoints3D, 203 | "Get the number of observations that see a triangulated point, i.e. " 204 | "the\n" 205 | "number of image points that have at least one correspondence to a\n" 206 | "triangulated point in another image.") 207 | .def("point3D_visibility_score", 208 | &Image::Point3DVisibilityScore, 209 | "Get the score of triangulated observations. In contrast to\n" 210 | "`NumVisiblePoints3D`, this score also captures the distribution\n" 211 | "of triangulated observations in the image. This is useful to " 212 | "select\n" 213 | "the next best image in incremental reconstruction, because a more\n" 214 | "uniform distribution of observations results in more robust " 215 | "registration.") 216 | .def("get_valid_point2D_ids", 217 | [](const Image& self) { 218 | std::vector valid_point2D_ids; 219 | 220 | for (point2D_t point2D_idx = 0; point2D_idx < self.NumPoints2D(); 221 | ++point2D_idx) { 222 | if (self.Point2D(point2D_idx).HasPoint3D()) { 223 | valid_point2D_ids.push_back(point2D_idx); 224 | } 225 | } 226 | 227 | return valid_point2D_ids; 228 | }) 229 | .def("get_valid_points2D", 230 | [](const Image& self) { 231 | std::vector valid_points2D; 232 | 233 | for (point2D_t point2D_idx = 0; point2D_idx < self.NumPoints2D(); 234 | ++point2D_idx) { 235 | if (self.Point2D(point2D_idx).HasPoint3D()) { 236 | valid_points2D.push_back(self.Point2D(point2D_idx)); 237 | } 238 | } 239 | 240 | return valid_points2D; 241 | }) 242 | .def("__repr__", &PrintImage); 243 | MakeDataclass(PyImage); 244 | } 245 | -------------------------------------------------------------------------------- /pycolmap/scene/point2D.h: -------------------------------------------------------------------------------- 1 | #include "colmap/scene/point2d.h" 2 | #include "colmap/util/misc.h" 3 | #include "colmap/util/types.h" 4 | 5 | #include "pycolmap/helpers.h" 6 | #include "pycolmap/log_exceptions.h" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace colmap; 18 | using namespace pybind11::literals; 19 | namespace py = pybind11; 20 | 21 | using Point2DVector = 22 | std::vector>; 23 | PYBIND11_MAKE_OPAQUE(Point2DVector); 24 | 25 | std::string PrintPoint2D(const Point2D& p2D) { 26 | std::stringstream ss; 27 | ss << "Point2D(xy=[" << p2D.xy.format(vec_fmt) << "], point3D_id=" 28 | << (p2D.HasPoint3D() ? std::to_string(p2D.point3D_id) : "Invalid") << ")"; 29 | return ss.str(); 30 | } 31 | 32 | void BindPoint2D(py::module& m) { 33 | py::bind_vector(m, "ListPoint2D") 34 | .def("__repr__", [](const Point2DVector& self) { 35 | std::string repr = "["; 36 | bool is_first = true; 37 | for (auto& p2D : self) { 38 | if (!is_first) { 39 | repr += ", "; 40 | } 41 | is_first = false; 42 | repr += PrintPoint2D(p2D); 43 | } 44 | repr += "]"; 45 | return repr; 46 | }); 47 | 48 | py::class_ext_> PyPoint2D(m, "Point2D"); 49 | PyPoint2D.def(py::init<>()) 50 | .def(py::init(), 51 | "xy"_a, 52 | "point3D_id"_a = kInvalidPoint3DId) 53 | .def_readwrite("xy", &Point2D::xy) 54 | .def_readwrite("point3D_id", &Point2D::point3D_id) 55 | .def("has_point3D", &Point2D::HasPoint3D) 56 | .def("__repr__", &PrintPoint2D); 57 | MakeDataclass(PyPoint2D); 58 | } 59 | -------------------------------------------------------------------------------- /pycolmap/scene/point3D.h: -------------------------------------------------------------------------------- 1 | #include "colmap/scene/point3d.h" 2 | #include "colmap/util/misc.h" 3 | #include "colmap/util/types.h" 4 | 5 | #include "pycolmap/helpers.h" 6 | #include "pycolmap/log_exceptions.h" 7 | #include "pycolmap/pybind11_extension.h" 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace colmap; 17 | namespace py = pybind11; 18 | 19 | using Point3DMap = std::unordered_map; 20 | PYBIND11_MAKE_OPAQUE(Point3DMap); 21 | 22 | void BindPoint3D(py::module& m) { 23 | py::bind_map_fix(m, "MapPoint3DIdToPoint3D") 24 | .def("__repr__", [](const Point3DMap& self) { 25 | return "MapPoint3DIdToPoint3D(num_points3D=" + 26 | std::to_string(self.size()) + ")"; 27 | }); 28 | 29 | py::class_ext_> PyPoint3D(m, "Point3D"); 30 | PyPoint3D.def(py::init<>()) 31 | .def_readwrite("xyz", &Point3D::xyz) 32 | .def_readwrite("color", &Point3D::color) 33 | .def_readwrite("error", &Point3D::error) 34 | .def_readwrite("track", &Point3D::track) 35 | .def("__repr__", [](const Point3D& self) { 36 | std::stringstream ss; 37 | ss << "Point3D(xyz=[" << self.xyz.format(vec_fmt) << "], color=[" 38 | << self.color.format(vec_fmt) << "], error=" << self.error 39 | << ", track=Track(length=" << self.track.Length() << "))"; 40 | return ss.str(); 41 | }); 42 | MakeDataclass(PyPoint3D); 43 | } 44 | -------------------------------------------------------------------------------- /pycolmap/scene/track.h: -------------------------------------------------------------------------------- 1 | #include "colmap/scene/track.h" 2 | #include "colmap/util/misc.h" 3 | #include "colmap/util/types.h" 4 | 5 | #include "pycolmap/helpers.h" 6 | #include "pycolmap/log_exceptions.h" 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | using namespace colmap; 16 | using namespace pybind11::literals; 17 | namespace py = pybind11; 18 | 19 | void BindTrack(py::module& m) { 20 | py::class_> PyTrackElement( 21 | m, "TrackElement"); 22 | PyTrackElement.def(py::init<>()) 23 | .def(py::init()) 24 | .def_readwrite("image_id", &TrackElement::image_id) 25 | .def_readwrite("point2D_idx", &TrackElement::point2D_idx) 26 | .def("__repr__", [](const TrackElement& self) { 27 | return "TrackElement(image_id=" + std::to_string(self.image_id) + 28 | ", point2D_idx=" + std::to_string(self.point2D_idx) + ")"; 29 | }); 30 | MakeDataclass(PyTrackElement); 31 | 32 | py::class_> PyTrack(m, "Track"); 33 | PyTrack.def(py::init<>()) 34 | .def(py::init([](const std::vector& elements) { 35 | auto track = std::make_shared(); 36 | track->AddElements(elements); 37 | return track; 38 | })) 39 | .def("length", &Track::Length, "Track Length.") 40 | .def("add_element", 41 | py::overload_cast(&Track::AddElement), 42 | "Add observation (image_id, point2D_idx) to track.") 43 | .def("delete_element", 44 | py::overload_cast(&Track::DeleteElement), 45 | "Delete observation (image_id, point2D_idx) from track.") 46 | .def("append", py::overload_cast(&Track::AddElement)) 47 | .def( 48 | "add_element", 49 | py::overload_cast(&Track::AddElement)) 50 | .def("add_elements", &Track::AddElements, "Add TrackElement list.") 51 | .def( 52 | "remove", 53 | [](Track& self, const size_t idx) { 54 | THROW_CHECK_LT(idx, self.Elements().size()); 55 | self.DeleteElement(idx); 56 | }, 57 | "Remove TrackElement at index.") 58 | .def_property("elements", 59 | py::overload_cast<>(&Track::Elements), 60 | &Track::SetElements) 61 | .def("remove", 62 | py::overload_cast( 63 | &Track::DeleteElement), 64 | "Remove TrackElement with (image_id,point2D_idx).") 65 | .def("__repr__", [](const Track& self) { 66 | return "Track(length=" + std::to_string(self.Length()) + ")"; 67 | }); 68 | MakeDataclass(PyTrack); 69 | } 70 | -------------------------------------------------------------------------------- /pycolmap/sfm/bindings.h: -------------------------------------------------------------------------------- 1 | #include "pycolmap/sfm/incremental_mapper.h" 2 | #include "pycolmap/sfm/incremental_triangulator.h" 3 | 4 | #include 5 | 6 | namespace py = pybind11; 7 | 8 | void BindSfMObjects(py::module& m) { 9 | BindIncrementalTriangulator(m); 10 | BindIncrementalMapper(m); 11 | } 12 | -------------------------------------------------------------------------------- /pycolmap/sfm/incremental_mapper.h: -------------------------------------------------------------------------------- 1 | #include "colmap/sfm/incremental_mapper.h" 2 | 3 | #include "pycolmap/helpers.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace colmap; 10 | namespace py = pybind11; 11 | 12 | void BindIncrementalMapper(py::module& m) { 13 | using ImageSelection = IncrementalMapper::Options::ImageSelectionMethod; 14 | auto PyImageSelectionMethod = 15 | py::enum_(m, "ImageSelectionMethod") 16 | .value("MAX_VISIBLE_POINTS_NUM", 17 | ImageSelection::MAX_VISIBLE_POINTS_NUM) 18 | .value("MAX_VISIBLE_POINTS_RATIO", 19 | ImageSelection::MAX_VISIBLE_POINTS_RATIO) 20 | .value("MIN_UNCERTAINTY", ImageSelection::MIN_UNCERTAINTY); 21 | AddStringToEnumConstructor(PyImageSelectionMethod); 22 | 23 | using Opts = IncrementalMapper::Options; 24 | auto PyOpts = py::class_(m, "IncrementalMapperOptions"); 25 | PyOpts.def(py::init<>()) 26 | .def_readwrite("init_min_num_inliers", 27 | &Opts::init_min_num_inliers, 28 | "Minimum number of inliers for initial image pair.") 29 | .def_readwrite("init_max_error", 30 | &Opts::init_max_error, 31 | "Maximum error in pixels for two-view geometry estimation " 32 | "for initial image pair.") 33 | .def_readwrite("init_max_forward_motion", 34 | &Opts::init_max_forward_motion, 35 | "Maximum forward motion for initial image pair.") 36 | .def_readwrite("init_min_tri_angle", 37 | &Opts::init_min_tri_angle, 38 | "Minimum triangulation angle for initial image pair.") 39 | .def_readwrite( 40 | "init_max_reg_trials", 41 | &Opts::init_max_reg_trials, 42 | "Maximum number of trials to use an image for initialization.") 43 | .def_readwrite("abs_pose_max_error", 44 | &Opts::abs_pose_max_error, 45 | "Maximum reprojection error in absolute pose estimation.") 46 | .def_readwrite("abs_pose_min_num_inliers", 47 | &Opts::abs_pose_min_num_inliers, 48 | "Minimum number of inliers in absolute pose estimation.") 49 | .def_readwrite("abs_pose_min_inlier_ratio", 50 | &Opts::abs_pose_min_inlier_ratio, 51 | "Minimum inlier ratio in absolute pose estimation.") 52 | .def_readwrite( 53 | "abs_pose_refine_focal_length", 54 | &Opts::abs_pose_refine_focal_length, 55 | "Whether to estimate the focal length in absolute pose estimation.") 56 | .def_readwrite("abs_pose_refine_extra_params", 57 | &Opts::abs_pose_refine_extra_params, 58 | "Whether to estimate the extra parameters in absolute " 59 | "pose estimation.") 60 | .def_readwrite("local_ba_num_images", 61 | &Opts::local_ba_num_images, 62 | "Number of images to optimize in local bundle adjustment.") 63 | .def_readwrite("local_ba_min_tri_angle", 64 | &Opts::local_ba_min_tri_angle, 65 | "Minimum triangulation for images to be chosen in local " 66 | "bundle adjustment.") 67 | .def_readwrite("min_focal_length_ratio", 68 | &Opts::min_focal_length_ratio, 69 | "The threshold used to filter and ignore images with " 70 | "degenerate intrinsics.") 71 | .def_readwrite("max_focal_length_ratio", 72 | &Opts::max_focal_length_ratio, 73 | "The threshold used to filter and ignore images with " 74 | "degenerate intrinsics.") 75 | .def_readwrite("max_extra_param", 76 | &Opts::max_extra_param, 77 | "The threshold used to filter and ignore images with " 78 | "degenerate intrinsics.") 79 | .def_readwrite("filter_max_reproj_error", 80 | &Opts::filter_max_reproj_error, 81 | "Maximum reprojection error in pixels for observations.") 82 | .def_readwrite( 83 | "filter_min_tri_angle", 84 | &Opts::filter_min_tri_angle, 85 | "Minimum triangulation angle in degrees for stable 3D points.") 86 | .def_readwrite("max_reg_trials", 87 | &Opts::max_reg_trials, 88 | "Maximum number of trials to register an image.") 89 | .def_readwrite("fix_existing_images", 90 | &Opts::fix_existing_images, 91 | "If reconstruction is provided as input, fix the existing " 92 | "image poses.") 93 | .def_readwrite("num_threads", &Opts::num_threads, "Number of threads.") 94 | .def_readwrite("image_selection_method", 95 | &Opts::image_selection_method, 96 | "Method to find and select next best image to register."); 97 | MakeDataclass(PyOpts); 98 | } 99 | -------------------------------------------------------------------------------- /pycolmap/sfm/incremental_triangulator.h: -------------------------------------------------------------------------------- 1 | #include "colmap/sfm/incremental_triangulator.h" 2 | 3 | #include "pycolmap/helpers.h" 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace colmap; 13 | namespace py = pybind11; 14 | 15 | void BindIncrementalTriangulator(py::module& m) { 16 | using Opts = IncrementalTriangulator::Options; 17 | auto PyOpts = py::class_(m, "IncrementalTriangulatorOptions"); 18 | PyOpts.def(py::init<>()) 19 | .def_readwrite("max_transitivity", 20 | &Opts::max_transitivity, 21 | "Maximum transitivity to search for correspondences.") 22 | .def_readwrite("create_max_angle_error", 23 | &Opts::create_max_angle_error, 24 | "Maximum angular error to create new triangulations.") 25 | .def_readwrite( 26 | "continue_max_angle_error", 27 | &Opts::continue_max_angle_error, 28 | "Maximum angular error to continue existing triangulations.") 29 | .def_readwrite( 30 | "merge_max_reproj_error", 31 | &Opts::merge_max_reproj_error, 32 | "Maximum reprojection error in pixels to merge triangulations.") 33 | .def_readwrite( 34 | "complete_max_reproj_error", 35 | &Opts::complete_max_reproj_error, 36 | "Maximum reprojection error to complete an existing triangulation.") 37 | .def_readwrite("complete_max_transitivity", 38 | &Opts::complete_max_transitivity, 39 | "Maximum transitivity for track completion.") 40 | .def_readwrite("re_max_angle_error", 41 | &Opts::re_max_angle_error, 42 | "Maximum angular error to re-triangulate " 43 | "under-reconstructed image pairs.") 44 | .def_readwrite("re_min_ratio", 45 | &Opts::re_min_ratio, 46 | "Minimum ratio of common triangulations between an image " 47 | "pair over the number of correspondences between that " 48 | "image pair to be considered as under-reconstructed.") 49 | .def_readwrite( 50 | "re_max_trials", 51 | &Opts::re_max_trials, 52 | "Maximum number of trials to re-triangulate an image pair.") 53 | .def_readwrite( 54 | "min_angle", 55 | &Opts::min_angle, 56 | "Minimum pairwise triangulation angle for a stable triangulation.") 57 | .def_readwrite("ignore_two_view_tracks", 58 | &Opts::ignore_two_view_tracks, 59 | "Whether to ignore two-view tracks.") 60 | .def_readwrite("min_focal_length_ratio", 61 | &Opts::min_focal_length_ratio, 62 | "The threshold used to filter and ignore images with " 63 | "degenerate intrinsics.") 64 | .def_readwrite("max_focal_length_ratio", 65 | &Opts::max_focal_length_ratio, 66 | "The threshold used to filter and ignore images with " 67 | "degenerate intrinsics.") 68 | .def_readwrite("max_extra_param", 69 | &Opts::max_extra_param, 70 | "The threshold used to filter and ignore images with " 71 | "degenerate intrinsics."); 72 | MakeDataclass(PyOpts); 73 | 74 | // TODO: Add bindings for GetModifiedPoints3D. 75 | // TODO: Add bindings for Find, Create, Continue, Merge, Complete, 76 | // HasCameraBogusParams once they become public. 77 | py::class_>( 78 | m, "IncrementalTriangulator") 79 | .def(py::init, 80 | std::shared_ptr>()) 81 | .def("triangulate_image", &IncrementalTriangulator::TriangulateImage) 82 | .def("complete_image", &IncrementalTriangulator::CompleteImage) 83 | .def("complete_all_tracks", &IncrementalTriangulator::CompleteAllTracks) 84 | .def("merge_all_tracks", &IncrementalTriangulator::MergeAllTracks) 85 | .def("retriangulate", &IncrementalTriangulator::Retriangulate) 86 | .def("add_modified_point3D", &IncrementalTriangulator::AddModifiedPoint3D) 87 | .def("clear_modified_points3D", 88 | &IncrementalTriangulator::ClearModifiedPoints3D) 89 | .def("merge_tracks", &IncrementalTriangulator::MergeTracks) 90 | .def("complete_tracks", &IncrementalTriangulator::CompleteTracks) 91 | .def("__copy__", 92 | [](const IncrementalTriangulator& self) { 93 | return IncrementalTriangulator(self); 94 | }) 95 | .def("__deepcopy__", 96 | [](const IncrementalTriangulator& self, const py::dict&) { 97 | return IncrementalTriangulator(self); 98 | }) 99 | .def("__repr__", [](const IncrementalTriangulator& self) { 100 | // TODO: Print reconstruction and correspondence_graph once public. 101 | return "IncrementalTriangulator()"; 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /pycolmap/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pycolmap/log_exceptions.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | enum class Device { AUTO = -1, CPU = 0, CUDA = 1 }; 10 | 11 | bool IsGPU(Device device) { 12 | if (device == Device::AUTO) { 13 | #ifdef COLMAP_CUDA_ENABLED 14 | return true; 15 | #else 16 | return false; 17 | #endif 18 | } else { 19 | return static_cast(device); 20 | } 21 | } 22 | 23 | void VerifyGPUParams(const bool use_gpu) { 24 | #ifndef COLMAP_CUDA_ENABLED 25 | if (use_gpu) { 26 | THROW_EXCEPTION(std::invalid_argument, 27 | "Cannot use Sift GPU without CUDA support; " 28 | "set device='auto' or device='cpu'.") 29 | } 30 | #endif 31 | } 32 | 33 | typedef Eigen::Matrix PyInlierMask; 34 | 35 | PyInlierMask ToPythonMask(const std::vector& mask_char) { 36 | return Eigen::Map>( 37 | mask_char.data(), mask_char.size()) 38 | .cast(); 39 | } 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["scikit-build-core>=0.3.3", "pybind11==2.11.1"] 3 | build-backend = "scikit_build_core.build" 4 | 5 | 6 | [project] 7 | name = "pycolmap" 8 | version = "0.7.0-dev" 9 | description="COLMAP bindings" 10 | readme = "README.md" 11 | authors = [ 12 | { name = "Mihai Dusmanu", email = "mihai.dusmanu@microsoft.com" }, 13 | { name = "Paul-Edouard Sarlin", email = "psarlin@ethz.ch" }, 14 | { name = "Philipp Lindenberger", email = "plindenbe@ethz.ch" }, 15 | ] 16 | license = {file = "LICENSE"} 17 | urls = {Repository = "https://github.com/colmap/pycolmap"} 18 | requires-python = ">=3.7" 19 | dependencies = ["numpy"] 20 | classifiers = [ 21 | "License :: OSI Approved :: BSD License", 22 | "Programming Language :: Python :: 3 :: Only", 23 | ] 24 | 25 | 26 | [tool.scikit-build] 27 | wheel.expand-macos-universal-tags = true 28 | 29 | 30 | [tool.cibuildwheel] 31 | build = "cp3{8,9,10,11}-{macosx,manylinux,win}*" 32 | archs = ["auto64"] 33 | test-command = "python -c \"import pycolmap; print(pycolmap.__version__)\"" 34 | 35 | [tool.cibuildwheel.environment] 36 | COLMAP_COMMIT_ID = "3.9.1" 37 | VCPKG_COMMIT_ID = "fa6e6a6ec3224f1d3697d544edef6272a59cd834" 38 | 39 | [tool.cibuildwheel.linux] 40 | before-all = "{project}/package/install-colmap-centos.sh" 41 | 42 | [tool.cibuildwheel.macos] 43 | before-all = "{project}/package/install-colmap-macos.sh" 44 | 45 | [tool.cibuildwheel.windows] 46 | before-all = "powershell -File {project}/package/install-colmap-windows.ps1" 47 | before-build = "pip install delvewheel" 48 | --------------------------------------------------------------------------------