├── .clang-format ├── .clang-tidy ├── .github └── workflows │ ├── install-ccache.ps1 │ ├── mac.yml │ ├── ubuntu.yml │ └── windows.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── FindDependencies.cmake ├── FindGlog.cmake ├── FindMETIS.cmake └── FindSuiteSparse.cmake ├── docs ├── getting_started.md └── rotation_averager.md ├── glomap ├── CMakeLists.txt ├── controllers │ ├── global_mapper.cc │ ├── global_mapper.h │ ├── global_mapper_test.cc │ ├── option_manager.cc │ ├── option_manager.h │ ├── rotation_averager.cc │ ├── rotation_averager.h │ ├── rotation_averager_test.cc │ ├── track_establishment.cc │ ├── track_establishment.h │ ├── track_retriangulation.cc │ └── track_retriangulation.h ├── estimators │ ├── bundle_adjustment.cc │ ├── bundle_adjustment.h │ ├── cost_function.h │ ├── global_positioning.cc │ ├── global_positioning.h │ ├── global_rotation_averaging.cc │ ├── global_rotation_averaging.h │ ├── gravity_refinement.cc │ ├── gravity_refinement.h │ ├── optimization_base.h │ ├── relpose_estimation.cc │ ├── relpose_estimation.h │ ├── view_graph_calibration.cc │ └── view_graph_calibration.h ├── exe │ ├── global_mapper.cc │ ├── global_mapper.h │ ├── rotation_averager.cc │ └── rotation_averager.h ├── glomap.cc ├── io │ ├── colmap_converter.cc │ ├── colmap_converter.h │ ├── colmap_io.cc │ ├── colmap_io.h │ ├── pose_io.cc │ └── pose_io.h ├── math │ ├── gravity.cc │ ├── gravity.h │ ├── l1_solver.h │ ├── rigid3d.cc │ ├── rigid3d.h │ ├── tree.cc │ ├── tree.h │ ├── two_view_geometry.cc │ ├── two_view_geometry.h │ └── union_find.h ├── processors │ ├── image_pair_inliers.cc │ ├── image_pair_inliers.h │ ├── image_undistorter.cc │ ├── image_undistorter.h │ ├── reconstruction_normalizer.cc │ ├── reconstruction_normalizer.h │ ├── reconstruction_pruning.cc │ ├── reconstruction_pruning.h │ ├── relpose_filter.cc │ ├── relpose_filter.h │ ├── track_filter.cc │ ├── track_filter.h │ ├── view_graph_manipulation.cc │ └── view_graph_manipulation.h ├── scene │ ├── camera.h │ ├── image.h │ ├── image_pair.h │ ├── track.h │ ├── types.h │ ├── types_sfm.h │ ├── view_graph.cc │ └── view_graph.h └── types.h ├── scripts ├── format │ └── c++.sh └── shell │ └── enter_vs_dev_shell.ps1 └── vcpkg.json /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | BinPackArguments: false 3 | BinPackParameters: false 4 | DerivePointerAlignment: false 5 | IncludeBlocks: Regroup 6 | IncludeCategories: 7 | - Regex: '^"glomap' 8 | Priority: 1 9 | - Regex: '^' 12 | Priority: 4 13 | - Regex: '.*' 14 | Priority: 5 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/install-ccache.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter(Mandatory = $true)] 4 | [string] $Destination 5 | ) 6 | 7 | $version = "4.10.2" 8 | $folder="ccache-$version-windows-x86_64" 9 | $url = "https://github.com/ccache/ccache/releases/download/v$version/$folder.zip" 10 | $expectedSha256 = "6252F081876A9A9F700FAE13A5AEC5D0D486B28261D7F1F72AC11C7AD9DF4DA9" 11 | 12 | $ErrorActionPreference = "Stop" 13 | 14 | try { 15 | New-Item -Path "$Destination" -ItemType Container -ErrorAction SilentlyContinue 16 | 17 | Write-Host "Download CCache" 18 | $zipFilePath = Join-Path "$env:TEMP" "$folder.zip" 19 | Invoke-WebRequest -Uri $url -UseBasicParsing -OutFile "$zipFilePath" -MaximumRetryCount 3 20 | 21 | $hash = Get-FileHash $zipFilePath -Algorithm "sha256" 22 | if ($hash.Hash -ne $expectedSha256) { 23 | throw "File $Path hash $hash.Hash did not match expected hash $expectedHash" 24 | } 25 | 26 | Write-Host "Unzip CCache" 27 | Expand-Archive -Path "$zipFilePath" -DestinationPath "$env:TEMP" 28 | 29 | Write-Host "Move CCache" 30 | Move-Item -Force "$env:TEMP/$folder/ccache.exe" "$Destination" 31 | Remove-Item "$zipFilePath" 32 | Remove-Item -Recurse "$env:TEMP/$folder" 33 | } 34 | catch { 35 | Write-Host "Installation failed with an error" 36 | $_.Exception | Format-List 37 | exit -1 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/mac.yml: -------------------------------------------------------------------------------- 1 | name: Mac 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [ assigned, opened, synchronize, reopened ] 9 | release: 10 | types: [ published, edited ] 11 | 12 | jobs: 13 | build: 14 | name: ${{ matrix.config.os }} ${{ matrix.config.arch }} ${{ matrix.config.cmakeBuildType }} 15 | runs-on: ${{ matrix.config.os }} 16 | strategy: 17 | matrix: 18 | config: [ 19 | { 20 | os: macos-14, 21 | arch: arm64, 22 | cmakeBuildType: Release, 23 | }, 24 | ] 25 | 26 | env: 27 | COMPILER_CACHE_VERSION: 1 28 | COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache 29 | CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache 30 | CCACHE_BASEDIR: ${{ github.workspace }} 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/cache@v4 35 | id: cache-builds 36 | with: 37 | key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ matrix.config.cmakeBuildType }}-${{ github.run_id }}-${{ github.run_number }} 38 | restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ matrix.config.cmakeBuildType }} 39 | path: ${{ env.COMPILER_CACHE_DIR }} 40 | 41 | - name: Setup Mac 42 | run: | 43 | brew install \ 44 | cmake \ 45 | ninja \ 46 | boost \ 47 | eigen \ 48 | flann \ 49 | freeimage \ 50 | metis \ 51 | glog \ 52 | googletest \ 53 | ceres-solver \ 54 | qt5 \ 55 | glew \ 56 | cgal \ 57 | sqlite3 \ 58 | ccache 59 | 60 | - name: Configure and build 61 | run: | 62 | cmake --version 63 | mkdir build 64 | cd build 65 | cmake .. \ 66 | -GNinja \ 67 | -DCMAKE_BUILD_TYPE=${{ matrix.config.cmakeBuildType }} \ 68 | -DTESTS_ENABLED=ON \ 69 | -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)" 70 | ninja 71 | 72 | - name: Run tests 73 | run: | 74 | cd build 75 | set +e 76 | ctest --output-on-failure -E .+colmap_.* 77 | 78 | - name: Cleanup compiler cache 79 | run: | 80 | set -x 81 | ccache --show-stats --verbose 82 | ccache --evict-older-than 1d 83 | ccache --show-stats --verbose 84 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [ assigned, opened, synchronize, reopened ] 9 | release: 10 | types: [ published, edited ] 11 | 12 | jobs: 13 | build: 14 | name: ${{ matrix.config.os }} ${{ matrix.config.cmakeBuildType }} ${{ matrix.config.cudaEnabled && 'CUDA' || '' }} ${{ matrix.config.asanEnabled && 'ASan' || '' }} 15 | runs-on: ${{ matrix.config.os }} 16 | strategy: 17 | matrix: 18 | config: [ 19 | { 20 | os: ubuntu-24.04, 21 | cmakeBuildType: RelWithDebInfo, 22 | asanEnabled: false, 23 | cudaEnabled: false, 24 | checkCodeFormat: true, 25 | }, 26 | { 27 | os: ubuntu-22.04, 28 | cmakeBuildType: Release, 29 | asanEnabled: false, 30 | cudaEnabled: false, 31 | checkCodeFormat: true, 32 | }, 33 | { 34 | os: ubuntu-22.04, 35 | cmakeBuildType: Release, 36 | asanEnabled: false, 37 | cudaEnabled: true, 38 | checkCodeFormat: false, 39 | }, 40 | { 41 | os: ubuntu-22.04, 42 | cmakeBuildType: Release, 43 | asanEnabled: true, 44 | cudaEnabled: false, 45 | checkCodeFormat: false, 46 | }, 47 | { 48 | os: ubuntu-22.04, 49 | cmakeBuildType: ClangTidy, 50 | asanEnabled: false, 51 | cudaEnabled: false, 52 | checkCodeFormat: false, 53 | }, 54 | ] 55 | 56 | env: 57 | COMPILER_CACHE_VERSION: 1 58 | COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache 59 | CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache 60 | CCACHE_BASEDIR: ${{ github.workspace }} 61 | CTCACHE_DIR: ${{ github.workspace }}/compiler-cache/ctcache 62 | 63 | steps: 64 | - uses: actions/checkout@v4 65 | - uses: actions/cache@v4 66 | id: cache-builds 67 | with: 68 | key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }}-${{ github.run_id }}-${{ github.run_number }} 69 | restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }} 70 | path: ${{ env.COMPILER_CACHE_DIR }} 71 | - name: Install compiler cache 72 | run: | 73 | mkdir -p "$CCACHE_DIR" "$CTCACHE_DIR" 74 | echo "$COMPILER_CACHE_DIR/bin" >> $GITHUB_PATH 75 | 76 | if [ -f "$COMPILER_CACHE_DIR/bin/ccache" ]; then 77 | exit 0 78 | fi 79 | 80 | set -x 81 | wget https://github.com/ccache/ccache/releases/download/v4.8.2/ccache-4.8.2-linux-x86_64.tar.xz 82 | echo "0b33f39766fe9db67f40418aed6a5b3d7b2f4f7fab025a8213264b77a2d0e1b1 ccache-4.8.2-linux-x86_64.tar.xz" | sha256sum --check 83 | tar xfv ccache-4.8.2-linux-x86_64.tar.xz 84 | mkdir -p "$COMPILER_CACHE_DIR/bin" 85 | mv ./ccache-4.8.2-linux-x86_64/ccache "$COMPILER_CACHE_DIR/bin" 86 | ctcache_commit_id="66c3614175fc650591488519333c411b2eac15a3" 87 | wget https://github.com/matus-chochlik/ctcache/archive/${ctcache_commit_id}.zip 88 | echo "108b087f156a9fe7da0c796de1ef73f5855d2a33a27983769ea39061359a40fc ${ctcache_commit_id}.zip" | sha256sum --check 89 | unzip "${ctcache_commit_id}.zip" 90 | mv ctcache-${ctcache_commit_id}/clang-tidy* "$COMPILER_CACHE_DIR/bin" 91 | 92 | - name: Check code format 93 | if: matrix.config.checkCodeFormat 94 | run: | 95 | set +x -euo pipefail 96 | python -m pip install clang-format==19.1.0 97 | ./scripts/format/c++.sh 98 | git diff --name-only 99 | git diff --exit-code || (echo "Code formatting failed" && exit 1) 100 | 101 | - name: Setup Ubuntu 102 | run: | 103 | sudo apt-get update && sudo apt-get install -y \ 104 | build-essential \ 105 | cmake \ 106 | ninja-build \ 107 | libboost-program-options-dev \ 108 | libboost-graph-dev \ 109 | libboost-system-dev \ 110 | libeigen3-dev \ 111 | libceres-dev \ 112 | libflann-dev \ 113 | libfreeimage-dev \ 114 | libmetis-dev \ 115 | libgoogle-glog-dev \ 116 | libgtest-dev \ 117 | libgmock-dev \ 118 | libsqlite3-dev \ 119 | libglew-dev \ 120 | qtbase5-dev \ 121 | libqt5opengl5-dev \ 122 | libcgal-dev \ 123 | libcgal-qt5-dev \ 124 | libgl1-mesa-dri \ 125 | libunwind-dev \ 126 | libcurl4-openssl-dev \ 127 | xvfb 128 | 129 | if [ "${{ matrix.config.cudaEnabled }}" == "true" ]; then 130 | if [ "${{ matrix.config.os }}" == "ubuntu-20.04" ]; then 131 | sudo apt-get install -y \ 132 | nvidia-cuda-toolkit \ 133 | nvidia-cuda-toolkit-gcc 134 | echo "CC=/usr/bin/cuda-gcc" >> $GITHUB_ENV 135 | echo "CXX=/usr/bin/cuda-g++" >> $GITHUB_ENV 136 | elif [ "${{ matrix.config.os }}" == "ubuntu-22.04" ]; then 137 | sudo apt-get install -y \ 138 | nvidia-cuda-toolkit \ 139 | nvidia-cuda-toolkit-gcc \ 140 | gcc-10 g++-10 141 | echo "CC=/usr/bin/gcc-10" >> $GITHUB_ENV 142 | echo "CXX=/usr/bin/g++-10" >> $GITHUB_ENV 143 | echo "CUDAHOSTCXX=/usr/bin/g++-10" >> $GITHUB_ENV 144 | fi 145 | fi 146 | 147 | if [ "${{ matrix.config.asanEnabled }}" == "true" ]; then 148 | sudo apt-get install -y clang-15 libomp-15-dev 149 | echo "CC=/usr/bin/clang-15" >> $GITHUB_ENV 150 | echo "CXX=/usr/bin/clang++-15" >> $GITHUB_ENV 151 | fi 152 | 153 | if [ "${{ matrix.config.cmakeBuildType }}" == "ClangTidy" ]; then 154 | sudo apt-get install -y clang-15 clang-tidy-15 libomp-15-dev 155 | echo "CC=/usr/bin/clang-15" >> $GITHUB_ENV 156 | echo "CXX=/usr/bin/clang++-15" >> $GITHUB_ENV 157 | fi 158 | 159 | - name: Upgrade CMake 160 | run: | 161 | CMAKE_VERSION=3.28.6 162 | CMAKE_DIR=cmake-${CMAKE_VERSION}-linux-x86_64 163 | wget https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_DIR}.tar.gz 164 | tar -xzf ${CMAKE_DIR}.tar.gz 165 | sudo cp -r ${CMAKE_DIR}/* /usr/local/ 166 | rm -rf ${CMAKE_DIR}* 167 | 168 | - name: Configure and build 169 | run: | 170 | set -x 171 | cmake --version 172 | mkdir build 173 | cd build 174 | cmake .. \ 175 | -GNinja \ 176 | -DCMAKE_BUILD_TYPE=${{ matrix.config.cmakeBuildType }} \ 177 | -DCMAKE_INSTALL_PREFIX=./install \ 178 | -DCMAKE_CUDA_ARCHITECTURES=50 \ 179 | -DCUDA_ENABLED=${{ matrix.config.cudaEnabled }} \ 180 | -DTESTS_ENABLED=ON \ 181 | -DASAN_ENABLED=${{ matrix.config.asanEnabled }} 182 | ninja -k 10000 183 | 184 | - name: Run tests 185 | if: ${{ matrix.config.cmakeBuildType != 'ClangTidy' }} 186 | run: | 187 | export DISPLAY=":99.0" 188 | export QT_QPA_PLATFORM="offscreen" 189 | Xvfb :99 & 190 | sleep 3 191 | cd build 192 | ctest --output-on-failure -E .+colmap_.* 193 | 194 | - name: Cleanup compiler cache 195 | run: | 196 | set -x 197 | ccache --show-stats --verbose 198 | ccache --evict-older-than 1d 199 | ccache --show-stats --verbose 200 | 201 | echo "Size of ctcache before: $(du -sh $CTCACHE_DIR)" 202 | echo "Number of ctcache files before: $(find $CTCACHE_DIR | wc -l)" 203 | # Delete cache older than 10 days. 204 | find "$CTCACHE_DIR"/*/ -mtime +10 -print0 | xargs -0 rm -rf 205 | echo "Size of ctcache after: $(du -sh $CTCACHE_DIR)" 206 | echo "Number of ctcache files after: $(find $CTCACHE_DIR | wc -l)" 207 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [ assigned, opened, synchronize, reopened ] 9 | release: 10 | types: [ published, edited ] 11 | 12 | jobs: 13 | build: 14 | name: ${{ matrix.config.os }} ${{ matrix.config.cmakeBuildType }} ${{ matrix.config.cudaEnabled && 'CUDA' || '' }} 15 | runs-on: ${{ matrix.config.os }} 16 | strategy: 17 | matrix: 18 | config: [ 19 | { 20 | os: windows-2022, 21 | cmakeBuildType: Release, 22 | cudaEnabled: true, 23 | testsEnabled: true, 24 | exportPackage: true, 25 | }, 26 | { 27 | os: windows-2022, 28 | cmakeBuildType: Release, 29 | cudaEnabled: false, 30 | testsEnabled: true, 31 | exportPackage: true, 32 | }, 33 | ] 34 | 35 | env: 36 | COMPILER_CACHE_VERSION: 1 37 | COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache 38 | CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache 39 | CCACHE_BASEDIR: ${{ github.workspace }} 40 | VCPKG_COMMIT_ID: bc3512a509f9d29b37346a7e7e929f9a26e66c7e 41 | GLOG_v: 1 42 | GLOG_logtostderr: 1 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | # We define the vcpkg binary sources using separate variables for read and 48 | # write operations: 49 | # * Read sources are defined as inline. These can be read by anyone and, 50 | # in particular, pull requests from forks. Unfortunately, we cannot 51 | # define these as action environment variables. See: 52 | # https://github.com/orgs/community/discussions/44322 53 | # * Write sources are defined as action secret variables. These cannot be 54 | # read by pull requests from forks but only from pull requests from 55 | # within the target repository (i.e., created by a repository owner). 56 | # This protects us from malicious actors accessing our secrets and 57 | # gaining write access to our binary cache. For more information, see: 58 | # https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ 59 | - name: Setup vcpkg binary cache 60 | shell: pwsh 61 | run: | 62 | # !!!PLEASE!!! be nice and don't use this cache for your own purposes. This is only meant for CI purposes in this repository. 63 | $VCPKG_BINARY_SOURCES = "clear;x-azblob,https://colmap.blob.core.windows.net/github-actions-cache,sp=r&st=2024-12-10T17:29:32Z&se=2030-12-31T01:29:32Z&spr=https&sv=2022-11-02&sr=c&sig=bWydkilTMjRn3LHKTxLgdWrFpV4h%2Finzoe9QCOcPpYQ%3D,read" 64 | if ("${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }}") { 65 | # The secrets are only accessible in runs triggered from within the target repository and not forks. 66 | $VCPKG_BINARY_SOURCES += ";x-azblob,${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }},${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_SAS }},write" 67 | } 68 | echo "VCPKG_BINARY_SOURCES=${VCPKG_BINARY_SOURCES}" >> "${env:GITHUB_ENV}" 69 | 70 | - name: Compiler cache 71 | uses: actions/cache@v4 72 | id: cache-builds 73 | with: 74 | key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }}-${{ github.run_id }}-${{ github.run_number }} 75 | restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }} 76 | path: ${{ env.COMPILER_CACHE_DIR }} 77 | 78 | - name: Install ccache 79 | shell: pwsh 80 | run: | 81 | New-Item -ItemType Directory -Force -Path "${{ env.CCACHE_DIR }}" 82 | echo "${{ env.COMPILER_CACHE_DIR }}/bin" | Out-File -Encoding utf8 -Append -FilePath $env:GITHUB_PATH 83 | if (Test-Path -PathType Leaf "${{ env.COMPILER_CACHE_DIR }}/bin/ccache.exe") { 84 | exit 85 | } 86 | .github/workflows/install-ccache.ps1 -Destination "${{ env.COMPILER_CACHE_DIR }}/bin" 87 | 88 | - name: Install CUDA 89 | uses: Jimver/cuda-toolkit@v0.2.18 90 | if: matrix.config.cudaEnabled 91 | id: cuda-toolkit 92 | with: 93 | cuda: '12.6.2' 94 | sub-packages: '["nvcc", "nvtx", "cudart", "curand", "curand_dev", "nvrtc_dev"]' 95 | method: 'network' 96 | 97 | - name: Setup vcpkg 98 | shell: pwsh 99 | run: | 100 | ./scripts/shell/enter_vs_dev_shell.ps1 101 | cd ${{ github.workspace }} 102 | git clone https://github.com/microsoft/vcpkg 103 | cd vcpkg 104 | git reset --hard ${{ env.VCPKG_COMMIT_ID }} 105 | ./bootstrap-vcpkg.bat 106 | 107 | - name: Install CMake and Ninja 108 | uses: lukka/get-cmake@latest 109 | with: 110 | cmakeVersion: "3.31.0" 111 | ninjaVersion: "1.12.1" 112 | 113 | - name: Configure and build 114 | shell: pwsh 115 | run: | 116 | ./scripts/shell/enter_vs_dev_shell.ps1 117 | cd ${{ github.workspace }} 118 | ./vcpkg/vcpkg.exe integrate install 119 | mkdir build 120 | cd build 121 | cmake .. ` 122 | -GNinja ` 123 | -DCMAKE_MAKE_PROGRAM=ninja ` 124 | -DCMAKE_BUILD_TYPE=Release ` 125 | -DTESTS_ENABLED=ON ` 126 | -DCUDA_ENABLED=${{ matrix.config.cudaEnabled }} ` 127 | -DGUI_ENABLED=OFF ` 128 | -DCGAL_ENABLED=OFF ` 129 | -DCMAKE_CUDA_ARCHITECTURES=all-major ` 130 | -DCUDAToolkit_ROOT="${{ steps.cuda-toolkit.outputs.CUDA_PATH }}" ` 131 | -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" ` 132 | -DVCPKG_TARGET_TRIPLET=x64-windows-release ` 133 | -DCMAKE_INSTALL_PREFIX=install 134 | ninja 135 | 136 | - name: Run tests 137 | shell: pwsh 138 | run: | 139 | ./vcpkg/vcpkg.exe integrate install 140 | cd build 141 | ctest -E .+colmap_.* --output-on-failure 142 | 143 | - name: Export package 144 | if: matrix.config.exportPackage 145 | shell: pwsh 146 | run: | 147 | ./vcpkg/vcpkg.exe integrate install 148 | 149 | cd build 150 | ninja install 151 | 152 | ../vcpkg/vcpkg.exe install ` 153 | --triplet=x64-windows-release 154 | $(if ($${{ matrix.config.cudaEnabled }}) { echo "--x-feature=cuda" }) 155 | ../vcpkg/vcpkg.exe export --raw --output-dir vcpkg_export --output glomap 156 | cp vcpkg_export/glomap/installed/x64-windows/bin/*.dll install/bin 157 | cp vcpkg_export/glomap/installed/x64-windows-release/bin/*.dll install/bin 158 | if ($${{ matrix.config.cudaEnabled }}) { 159 | cp "${{ steps.cuda-toolkit.outputs.CUDA_PATH }}/bin/cudart64_*.dll" install/bin 160 | cp "${{ steps.cuda-toolkit.outputs.CUDA_PATH }}/bin/curand64_*.dll" install/bin 161 | } 162 | 163 | - name: Upload package 164 | uses: actions/upload-artifact@v4 165 | if: ${{ matrix.config.exportPackage && matrix.config.cudaEnabled }} 166 | with: 167 | name: glomap-x64-windows-cuda 168 | path: build/install 169 | 170 | - name: Upload package 171 | uses: actions/upload-artifact@v4 172 | if: ${{ matrix.config.exportPackage && !matrix.config.cudaEnabled }} 173 | with: 174 | name: glomap-x64-windows-nocuda 175 | path: build/install 176 | 177 | - name: Cleanup compiler cache 178 | shell: pwsh 179 | run: | 180 | ccache --show-stats --verbose 181 | ccache --evict-older-than 1d 182 | ccache --show-stats --verbose 183 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /data 3 | /.vscode 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.28) 2 | 3 | option(CUDA_ENABLED "Whether to enable CUDA, if available" ON) 4 | option(TESTS_ENABLED "Whether to build test binaries" OFF) 5 | option(ASAN_ENABLED "Whether to enable AddressSanitizer flags" OFF) 6 | option(CCACHE_ENABLED "Whether to enable compiler caching, if available" ON) 7 | option(FETCH_COLMAP "Whether to use COLMAP with FetchContent or with self-installed software" ON) 8 | option(FETCH_POSELIB "Whether to use PoseLib with FetchContent or with self-installed software" ON) 9 | 10 | # Propagate options to vcpkg manifest. 11 | if(CUDA_ENABLED) 12 | list(APPEND VCPKG_MANIFEST_FEATURES "cuda") 13 | endif() 14 | 15 | # Initialize the project. 16 | project(glomap VERSION 1.1.0) 17 | 18 | set(CMAKE_CXX_STANDARD 17) 19 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 20 | 21 | set_property(GLOBAL PROPERTY GLOBAL_DEPENDS_NO_CYCLES ON) 22 | 23 | include(cmake/FindDependencies.cmake) 24 | 25 | if (TESTS_ENABLED) 26 | enable_testing() 27 | endif() 28 | 29 | if(ASAN_ENABLED) 30 | message(STATUS "Enabling ASan") 31 | add_compile_options(-fsanitize=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope) 32 | add_link_options(-fsanitize=address) 33 | endif() 34 | 35 | if(CCACHE_ENABLED) 36 | find_program(CCACHE ccache) 37 | if(CCACHE) 38 | message(STATUS "Enabling ccache support") 39 | set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE}) 40 | set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE}) 41 | else() 42 | message(STATUS "Disabling ccache support") 43 | endif() 44 | else() 45 | message(STATUS "Disabling ccache support") 46 | endif() 47 | 48 | 49 | if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 50 | # Some fixes for the Glog library. 51 | add_definitions("-DGLOG_USE_GLOG_EXPORT") 52 | add_definitions("-DGLOG_NO_ABBREVIATED_SEVERITIES") 53 | add_definitions("-DGL_GLEXT_PROTOTYPES") 54 | add_definitions("-DNOMINMAX") 55 | add_compile_options(/EHsc) 56 | # Disable warning: 'initializing': conversion from 'X' to 'Y', possible loss of data 57 | add_compile_options(/wd4244 /wd4267 /wd4305) 58 | # Enable object level parallel builds in Visual Studio. 59 | add_compile_options(/MP) 60 | if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR "${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo") 61 | add_compile_options(/bigobj) 62 | endif() 63 | endif() 64 | 65 | add_subdirectory(glomap) 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024, ETH Zurich. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | * Neither the name of ETH Zurich nor the names of its contributors may 15 | be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GLOMAP: Global Structure-from-Motion Revisited 2 | 3 | [Project page](https://lpanaf.github.io/eccv24_glomap/) | [Paper](https://arxiv.org/pdf/2407.20219) 4 | --- 5 | 6 | ## About 7 | 8 | GLOMAP is a general purpose global structure-from-motion pipeline for 9 | image-based reconstruction. GLOMAP requires a COLMAP database as input and 10 | outputs a COLMAP sparse reconstruction. As compared to COLMAP, this project 11 | provides a much more efficient and scalable reconstruction process, typically 12 | 1-2 orders of magnitude faster, with on-par or superior reconstruction quality. 13 | 14 | If you use this project for your research, please cite 15 | ``` 16 | @inproceedings{pan2024glomap, 17 | author={Pan, Linfei and Barath, Daniel and Pollefeys, Marc and Sch\"{o}nberger, Johannes Lutz}, 18 | title={{Global Structure-from-Motion Revisited}}, 19 | booktitle={European Conference on Computer Vision (ECCV)}, 20 | year={2024}, 21 | } 22 | ``` 23 | To use the seperate rotation averaging module, refer to [this README](docs/rotation_averager.md). 24 | 25 | ## Getting Started 26 | 27 | To build GLOMAP, first install [COLMAP](https://colmap.github.io/install.html#build-from-source) 28 | dependencies and then build GLOMAP using the following commands: 29 | ```shell 30 | mkdir build 31 | cd build 32 | cmake .. -GNinja 33 | ninja && ninja install 34 | ``` 35 | Pre-compiled Windows binaries can be downloaded from the official 36 | [release page](https://github.com/colmap/glomap/releases). 37 | 38 | After installation, one can run GLOMAP by (starting from a database) 39 | ```shell 40 | glomap mapper --database_path DATABASE_PATH --output_path OUTPUT_PATH --image_path IMAGE_PATH 41 | ``` 42 | For more details on the command line interface, one can type `glomap -h` or `glomap mapper -h` for help. 43 | 44 | We also provide a guide on improving the obtained reconstruction, which can be found [here](docs/getting_started.md). 45 | 46 | Note: 47 | - GLOMAP depends on two external libraries - [COLMAP](https://github.com/colmap/colmap) and [PoseLib](https://github.com/PoseLib/PoseLib). 48 | With the default setting, the library is built automatically by GLOMAP via `FetchContent`. 49 | However, if a self-installed version is preferred, one can also disable the `FETCH_COLMAP` and `FETCH_POSELIB` CMake options. 50 | - To use `FetchContent`, the minimum required version of `cmake` is 3.28. If a self-installed version is used, `cmake` can be downgraded to 3.10. 51 | - If your system does not provide a recent enough CMake version, you can install it as: 52 | ```shell 53 | wget https://github.com/Kitware/CMake/releases/download/v3.30.1/cmake-3.30.1.tar.gz 54 | tar xfvz cmake-3.30.1.tar.gz && cd cmake-3.30.1 55 | ./bootstrap && make -j$(nproc) && sudo make install 56 | ``` 57 | 58 | ## End-to-End Example 59 | 60 | In this section, we will use datasets from [this link](https://demuc.de/colmap/datasets) as examples. 61 | Download the datasets and put them under `data` folder. 62 | 63 | ### From database 64 | 65 | If a COLMAP database already exists, GLOMAP can directly use it to perform mapping: 66 | ```shell 67 | glomap mapper \ 68 | --database_path ./data/gerrard-hall/database.db \ 69 | --image_path ./data/gerrard-hall/images \ 70 | --output_path ./output/gerrard-hall/sparse 71 | ``` 72 | 73 | ### From images 74 | 75 | To obtain a reconstruction from images, the database needs to be established 76 | first. Here, we utilize the functions from COLMAP: 77 | ```shell 78 | colmap feature_extractor \ 79 | --image_path ./data/south-building/images \ 80 | --database_path ./data/south-building/database.db 81 | colmap exhaustive_matcher \ 82 | --database_path ./data/south-building/database.db 83 | glomap mapper \ 84 | --database_path ./data/south-building/database.db \ 85 | --image_path ./data/south-building/images \ 86 | --output_path ./output/south-building/sparse 87 | ``` 88 | 89 | ### Visualize and use the results 90 | 91 | The results are written out in the COLMAP sparse reconstruction format. Please 92 | refer to [COLMAP](https://colmap.github.io/format.html#sparse-reconstruction) 93 | for more details. 94 | 95 | The reconstruction can be visualized using the COLMAP GUI, for example: 96 | ```shell 97 | colmap gui --import_path ./output/south-building/sparse/0 98 | ``` 99 | Alternatives like [rerun.io](https://rerun.io/examples/3d-reconstruction/glomap) 100 | also enable visualization of COLMAP and GLOMAP outputs. 101 | 102 | If you want to inspect the reconstruction programmatically, you can use 103 | `pycolmap` in Python or link against COLMAP's C++ library interface. 104 | 105 | ### Notes 106 | 107 | - For larger scale datasets, it is recommended to use `sequential_matcher` or 108 | `vocab_tree_matcher` from `COLMAP`. 109 | ```shell 110 | colmap sequential_matcher --database_path DATABASE_PATH 111 | colmap vocab_tree_matcher --database_path DATABASE_PATH --VocabTreeMatching.vocab_tree_path VOCAB_TREE_PATH 112 | ``` 113 | - Alternatively, one can use 114 | [hloc](https://github.com/cvg/Hierarchical-Localization/) for image retrieval 115 | and matching with learning-based descriptors. 116 | 117 | 118 | 119 | ## Acknowledgement 120 | 121 | We are highly inspired by COLMAP, PoseLib, Theia. Please consider also citing 122 | them, if using GLOMAP in your work. 123 | 124 | ## Support 125 | 126 | Please, use GitHub Discussions at https://github.com/colmap/glomap/discussions 127 | for questions and the GitHub issue tracker at https://github.com/colmap/glomap 128 | for bug reports, feature requests/additions, etc. 129 | 130 | ## Contribution 131 | 132 | Contributions (bug reports, bug fixes, improvements, etc.) are very welcome and 133 | should be submitted in the form of new issues and/or pull requests on GitHub. 134 | 135 | ## License 136 | 137 | ``` 138 | Copyright (c) 2024, ETH Zurich. 139 | All rights reserved. 140 | 141 | Redistribution and use in source and binary forms, with or without 142 | modification, are permitted provided that the following conditions are met: 143 | 144 | * Redistributions of source code must retain the above copyright 145 | notice, this list of conditions and the following disclaimer. 146 | 147 | * Redistributions in binary form must reproduce the above copyright 148 | notice, this list of conditions and the following disclaimer in the 149 | documentation and/or other materials provided with the distribution. 150 | 151 | * Neither the name of ETH Zurich nor the names of its contributors may 152 | be used to endorse or promote products derived from this software 153 | without specific prior written permission. 154 | 155 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 156 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 157 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 158 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE 159 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 160 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 161 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 162 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 165 | POSSIBILITY OF SUCH DAMAGE. 166 | ``` 167 | -------------------------------------------------------------------------------- /cmake/FindDependencies.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 2 | 3 | find_package(Eigen3 3.4 REQUIRED) 4 | find_package(CHOLMOD QUIET) 5 | if(NOT TARGET SuiteSparse::CHOLMOD) 6 | find_package(SuiteSparse COMPONENTS CHOLMOD REQUIRED) 7 | endif() 8 | find_package(Ceres REQUIRED COMPONENTS SuiteSparse) 9 | find_package(Boost REQUIRED) 10 | 11 | if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 12 | find_package(Glog REQUIRED) 13 | endif() 14 | 15 | if(DEFINED glog_VERSION_MAJOR) 16 | # Older versions of glog don't export version variables. 17 | add_definitions("-DGLOG_VERSION_MAJOR=${glog_VERSION_MAJOR}") 18 | add_definitions("-DGLOG_VERSION_MINOR=${glog_VERSION_MINOR}") 19 | endif() 20 | 21 | if(TESTS_ENABLED) 22 | message(STATUS "Enabling tests") 23 | find_package(GTest REQUIRED) 24 | endif() 25 | 26 | include(FetchContent) 27 | FetchContent_Declare(PoseLib 28 | GIT_REPOSITORY https://github.com/PoseLib/PoseLib.git 29 | GIT_TAG 0439b2d361125915b8821043fca9376e6cc575b9 30 | EXCLUDE_FROM_ALL 31 | SYSTEM 32 | ) 33 | message(STATUS "Configuring PoseLib...") 34 | if (FETCH_POSELIB) 35 | FetchContent_MakeAvailable(PoseLib) 36 | else() 37 | find_package(PoseLib REQUIRED) 38 | endif() 39 | message(STATUS "Configuring PoseLib... done") 40 | 41 | FetchContent_Declare(COLMAP 42 | GIT_REPOSITORY https://github.com/colmap/colmap.git 43 | GIT_TAG 78f1eefacae542d753c2e4f6a26771a0d976227d 44 | EXCLUDE_FROM_ALL 45 | ) 46 | message(STATUS "Configuring COLMAP...") 47 | set(UNINSTALL_ENABLED OFF CACHE INTERNAL "") 48 | if (FETCH_COLMAP) 49 | FetchContent_MakeAvailable(COLMAP) 50 | else() 51 | find_package(COLMAP REQUIRED) 52 | endif() 53 | message(STATUS "Configuring COLMAP... done") 54 | 55 | set(CUDA_MIN_VERSION "7.0") 56 | if(CUDA_ENABLED) 57 | if(CMAKE_VERSION VERSION_LESS 3.17) 58 | find_package(CUDA QUIET) 59 | if(CUDA_FOUND) 60 | message(STATUS "Found CUDA version ${CUDA_VERSION} installed in " 61 | "${CUDA_TOOLKIT_ROOT_DIR} via legacy CMake (<3.17) module. " 62 | "Using the legacy CMake module means that any installation of " 63 | "COLMAP will require that the CUDA libraries are " 64 | "available under LD_LIBRARY_PATH.") 65 | message(STATUS "Found CUDA ") 66 | message(STATUS " Includes : ${CUDA_INCLUDE_DIRS}") 67 | message(STATUS " Libraries : ${CUDA_LIBRARIES}") 68 | 69 | enable_language(CUDA) 70 | 71 | macro(declare_imported_cuda_target module) 72 | add_library(CUDA::${module} INTERFACE IMPORTED) 73 | target_include_directories( 74 | CUDA::${module} INTERFACE ${CUDA_INCLUDE_DIRS}) 75 | target_link_libraries( 76 | CUDA::${module} INTERFACE ${CUDA_${module}_LIBRARY} ${ARGN}) 77 | endmacro() 78 | 79 | declare_imported_cuda_target(cudart ${CUDA_LIBRARIES}) 80 | declare_imported_cuda_target(curand ${CUDA_LIBRARIES}) 81 | 82 | set(CUDAToolkit_VERSION "${CUDA_VERSION_STRING}") 83 | set(CUDAToolkit_BIN_DIR "${CUDA_TOOLKIT_ROOT_DIR}/bin") 84 | else() 85 | message(STATUS "CUDA not found") 86 | endif() 87 | else() 88 | find_package(CUDAToolkit QUIET) 89 | if(CUDAToolkit_FOUND) 90 | set(CUDA_FOUND ON) 91 | enable_language(CUDA) 92 | else() 93 | message(STATUS "CUDA not found") 94 | endif() 95 | endif() 96 | endif() 97 | 98 | if(CUDA_ENABLED AND CUDA_FOUND) 99 | if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES) 100 | set(CMAKE_CUDA_ARCHITECTURES "native") 101 | endif() 102 | 103 | add_definitions("-DGLOMAP_CUDA_ENABLED") 104 | 105 | # Do not show warnings if the architectures are deprecated. 106 | set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -Wno-deprecated-gpu-targets") 107 | # Explicitly set PIC flags for CUDA targets. 108 | if(NOT IS_MSVC) 109 | set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --compiler-options -fPIC") 110 | endif() 111 | 112 | message(STATUS "Enabling CUDA support (version: ${CUDAToolkit_VERSION}, " 113 | "archs: ${CMAKE_CUDA_ARCHITECTURES})") 114 | else() 115 | set(CUDA_ENABLED OFF) 116 | message(STATUS "Disabling CUDA support") 117 | endif() 118 | -------------------------------------------------------------------------------- /cmake/FindGlog.cmake: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, ETH Zurich and UNC Chapel Hill. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of 15 | # its contributors may be used to endorse or promote products derived 16 | # from this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE 22 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | # POSSIBILITY OF SUCH DAMAGE. 29 | 30 | 31 | # Find package module for Glog library. 32 | # 33 | # The following variables are set by this module: 34 | # 35 | # GLOG_FOUND: TRUE if Glog is found. 36 | # glog::glog: Imported target to link against. 37 | # 38 | # The following variables control the behavior of this module: 39 | # 40 | # GLOG_INCLUDE_DIR_HINTS: List of additional directories in which to 41 | # search for Glog includes. 42 | # GLOG_LIBRARY_DIR_HINTS: List of additional directories in which to 43 | # search for Glog libraries. 44 | 45 | set(GLOG_INCLUDE_DIR_HINTS "" CACHE PATH "Glog include directory") 46 | set(GLOG_LIBRARY_DIR_HINTS "" CACHE PATH "Glog library directory") 47 | 48 | unset(GLOG_FOUND) 49 | 50 | find_package(glog CONFIG QUIET) 51 | if(TARGET glog::glog) 52 | set(GLOG_FOUND TRUE) 53 | message(STATUS "Found Glog") 54 | message(STATUS " Target : glog::glog") 55 | else() 56 | # Older versions of glog don't come with a find_package config. 57 | # Fall back to custom logic to find the library and remap to imported target. 58 | 59 | include(FindPackageHandleStandardArgs) 60 | 61 | list(APPEND GLOG_CHECK_INCLUDE_DIRS 62 | /usr/local/include 63 | /usr/local/homebrew/include 64 | /opt/local/var/macports/software 65 | /opt/local/include 66 | /usr/include) 67 | list(APPEND GLOG_CHECK_PATH_SUFFIXES 68 | glog/include 69 | glog/Include 70 | Glog/include 71 | Glog/Include 72 | src/windows) 73 | 74 | list(APPEND GLOG_CHECK_LIBRARY_DIRS 75 | /usr/local/lib 76 | /usr/local/homebrew/lib 77 | /opt/local/lib 78 | /usr/lib) 79 | list(APPEND GLOG_CHECK_LIBRARY_SUFFIXES 80 | glog/lib 81 | glog/Lib 82 | Glog/lib 83 | Glog/Lib 84 | x64/Release) 85 | 86 | find_path(GLOG_INCLUDE_DIRS 87 | NAMES 88 | glog/logging.h 89 | PATHS 90 | ${GLOG_INCLUDE_DIR_HINTS} 91 | ${GLOG_CHECK_INCLUDE_DIRS} 92 | PATH_SUFFIXES 93 | ${GLOG_CHECK_PATH_SUFFIXES}) 94 | find_library(GLOG_LIBRARIES 95 | NAMES 96 | glog 97 | libglog 98 | PATHS 99 | ${GLOG_LIBRARY_DIR_HINTS} 100 | ${GLOG_CHECK_LIBRARY_DIRS} 101 | PATH_SUFFIXES 102 | ${GLOG_CHECK_LIBRARY_SUFFIXES}) 103 | 104 | if(GLOG_INCLUDE_DIRS AND GLOG_LIBRARIES) 105 | set(GLOG_FOUND TRUE) 106 | message(STATUS "Found Glog") 107 | message(STATUS " Includes : ${GLOG_INCLUDE_DIRS}") 108 | message(STATUS " Libraries : ${GLOG_LIBRARIES}") 109 | endif() 110 | 111 | add_library(glog::glog INTERFACE IMPORTED) 112 | target_include_directories(glog::glog INTERFACE ${GLOG_INCLUDE_DIRS}) 113 | target_link_libraries(glog::glog INTERFACE ${GLOG_LIBRARIES}) 114 | endif() 115 | 116 | if(NOT GLOG_FOUND AND GLOG_FIND_REQUIRED) 117 | message(FATAL_ERROR "Could not find Glog") 118 | endif() 119 | -------------------------------------------------------------------------------- /cmake/FindMETIS.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 Sergiu Deitsch 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTMETISLAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | # SOFTWARE. 21 | # 22 | #[=======================================================================[.rst: 23 | Module for locating METIS 24 | ========================= 25 | 26 | Read-only variables: 27 | 28 | ``METIS_FOUND`` 29 | Indicates whether the library has been found. 30 | 31 | ``METIS_VERSION`` 32 | Indicates library version. 33 | 34 | Targets 35 | ------- 36 | 37 | ``METIS::METIS`` 38 | Specifies targets that should be passed to target_link_libararies. 39 | ]=======================================================================] 40 | 41 | include (FindPackageHandleStandardArgs) 42 | 43 | find_path (METIS_INCLUDE_DIR NAMES metis.h 44 | PATH_SUFFIXES include 45 | DOC "METIS include directory") 46 | find_library (METIS_LIBRARY_DEBUG NAMES metis 47 | PATH_SUFFIXES Debug 48 | DOC "METIS debug library") 49 | find_library (METIS_LIBRARY_RELEASE NAMES metis 50 | PATH_SUFFIXES Release 51 | DOC "METIS release library") 52 | 53 | if (METIS_LIBRARY_RELEASE) 54 | if (METIS_LIBRARY_DEBUG) 55 | set (METIS_LIBRARY debug ${METIS_LIBRARY_DEBUG} optimized 56 | ${METIS_LIBRARY_RELEASE} CACHE STRING "METIS library") 57 | else (METIS_LIBRARY_DEBUG) 58 | set (METIS_LIBRARY ${METIS_LIBRARY_RELEASE} CACHE FILEPATH "METIS library") 59 | endif (METIS_LIBRARY_DEBUG) 60 | elseif (METIS_LIBRARY_DEBUG) 61 | set (METIS_LIBRARY ${METIS_LIBRARY_DEBUG} CACHE FILEPATH "METIS library") 62 | endif (METIS_LIBRARY_RELEASE) 63 | 64 | set (_METIS_VERSION_HEADER ${METIS_INCLUDE_DIR}/metis.h) 65 | 66 | if (EXISTS ${_METIS_VERSION_HEADER}) 67 | file (READ ${_METIS_VERSION_HEADER} _METIS_VERSION_CONTENTS) 68 | 69 | string (REGEX REPLACE ".*#define METIS_VER_MAJOR[ \t]+([0-9]+).*" "\\1" 70 | METIS_VERSION_MAJOR "${_METIS_VERSION_CONTENTS}") 71 | string (REGEX REPLACE ".*#define METIS_VER_MINOR[ \t]+([0-9]+).*" "\\1" 72 | METIS_VERSION_MINOR "${_METIS_VERSION_CONTENTS}") 73 | string (REGEX REPLACE ".*#define METIS_VER_SUBMINOR[ \t]+([0-9]+).*" "\\1" 74 | METIS_VERSION_PATCH "${_METIS_VERSION_CONTENTS}") 75 | 76 | set (METIS_VERSION 77 | ${METIS_VERSION_MAJOR}.${METIS_VERSION_MINOR}.${METIS_VERSION_PATCH}) 78 | set (METIS_VERSION_COMPONENTS 3) 79 | endif (EXISTS ${_METIS_VERSION_HEADER}) 80 | 81 | mark_as_advanced (METIS_INCLUDE_DIR METIS_LIBRARY_DEBUG METIS_LIBRARY_RELEASE 82 | METIS_LIBRARY) 83 | 84 | if (NOT TARGET METIS::METIS) 85 | if (METIS_INCLUDE_DIR OR METIS_LIBRARY) 86 | add_library (METIS::METIS IMPORTED UNKNOWN) 87 | endif (METIS_INCLUDE_DIR OR METIS_LIBRARY) 88 | endif (NOT TARGET METIS::METIS) 89 | 90 | if (METIS_INCLUDE_DIR) 91 | set_property (TARGET METIS::METIS PROPERTY INTERFACE_INCLUDE_DIRECTORIES 92 | ${METIS_INCLUDE_DIR}) 93 | endif (METIS_INCLUDE_DIR) 94 | 95 | if (METIS_LIBRARY_RELEASE) 96 | set_property (TARGET METIS::METIS PROPERTY IMPORTED_LOCATION_RELEASE 97 | ${METIS_LIBRARY_RELEASE}) 98 | set_property (TARGET METIS::METIS APPEND PROPERTY IMPORTED_CONFIGURATIONS 99 | RELEASE) 100 | endif (METIS_LIBRARY_RELEASE) 101 | 102 | if (METIS_LIBRARY_DEBUG) 103 | set_property (TARGET METIS::METIS PROPERTY IMPORTED_LOCATION_DEBUG 104 | ${METIS_LIBRARY_DEBUG}) 105 | set_property (TARGET METIS::METIS APPEND PROPERTY IMPORTED_CONFIGURATIONS 106 | DEBUG) 107 | endif (METIS_LIBRARY_DEBUG) 108 | 109 | find_package_handle_standard_args (METIS REQUIRED_VARS 110 | METIS_INCLUDE_DIR METIS_LIBRARY VERSION_VAR METIS_VERSION) 111 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## Installation and end-to-end examples 4 | 5 | Please refer to the main `README.md`. 6 | 7 | ## Recommended settings 8 | 9 | The default parameters do not always gaurantee satisfying reconstructions. 10 | Regarding this, there are several things which can generally help. 11 | 12 | ### Share camera parameters 13 | 14 | If images are known to be taken with the same physical camera under identical 15 | camera settings, or images are well organized and known to be taken by several 16 | cameras, it is higly recommended to share the camera intrinsics as appropriate. 17 | To achieve this, one can set `--ImageReader.single_camera_per_folder` or 18 | `--ImageReader.single_camera_per_image` in `colmap feature_extractor`. 19 | 20 | ### Handle high-resolution or blurry images 21 | 22 | If images have high resolution or are blurry, it is worth trying to increase the 23 | allowed epipolar error by modifying `--RelPoseEstimation.max_epipolar_error`. 24 | For example, increase it to 4 or 10. 25 | 26 | ### Speedup reconstruction process 27 | 28 | #### Cap the number of tracks 29 | 30 | If the number of images and points are large, the run-time of global bundle 31 | adjustment can be long. In this case, to further speed up the overall 32 | reconstruction process, the total number of points can be capped, by changing 33 | `--TrackEstablishment.max_num_tracks`. Typically, one image should not need more 34 | than 1000 tracks to achieve good performance, so this number can be adjusted to 35 | $1000 \times n$. Afterwards, if a full point cloud is desired (for example, to 36 | initialize a Gaussian Splatting), points can be triangulated directly by calling 37 | `colmap point_triangulator`. 38 | 39 | Note, if the `--skip_retriangulation` is not set when calling `glomap mapper`, 40 | retriangulation should already been performed. 41 | 42 | #### Limit optimization iterations 43 | 44 | The number of global positioning and bundle adjustment iterations can be limited 45 | using the `--GlobalPositioning.max_num_iterations` and 46 | `--BundleAdjustment.max_num_iterations` options. 47 | 48 | #### Enable GPU-based solver 49 | 50 | If Ceres 2.3 or above is installed and cuDSS is installed, GLOMAP supports GPU 51 | accelerated optimization. The process can be largely sped up with flags 52 | `--GlobalPositioning.use_gpu 1 --BundleAdjustment.use_gpu`. 53 | -------------------------------------------------------------------------------- /docs/rotation_averager.md: -------------------------------------------------------------------------------- 1 | # Gravity-aligned Rotation Averaging with Circular Regression 2 | 3 | [Project page](https://lpanaf.github.io/eccv24_ra1dof/) | [Paper](https://www.ecva.net/papers/eccv_2024/papers_ECCV/papers/05651.pdf) | [Supp.](https://lpanaf.github.io/assets/pdf/eccv24_ra1dof_sm.pdf) 4 | --- 5 | 6 | ## About 7 | 8 | This project aims at solving the rotation averaging problem with gravity prior. 9 | To achieve this, circular regression is leveraged. 10 | 11 | If you use this project for your research, please cite 12 | ``` 13 | @inproceedings{pan2024ra1dof, 14 | author={Pan, Linfei and Pollefeys, Marc and Barath, Daniel}, 15 | title={{Gravity-aligned Rotation Averaging with Circular Regression}}, 16 | booktitle={European Conference on Computer Vision (ECCV)}, 17 | year={2024}, 18 | } 19 | ``` 20 | 21 | ## Getting Started 22 | Install GLOMAP as instrcucted in [README](../README.md). 23 | Then, call the rotation averager (3 degree-of-freedom) via 24 | ``` 25 | glomap rotation_averager --relpose_path RELPOSE_PATH --output_path OUTPUT_PATH 26 | ``` 27 | 28 | If gravity directions are available, call the rotation averager (1 degree-of-freedom) via 29 | ``` 30 | glomap rotation_averager \ 31 | --relpose_path RELPOSE_PATH \ 32 | --output_path OUTPUT_PATH \ 33 | --gravity_path GRAVTIY PATH 34 | ``` 35 | It is recommended to set `--use_stratified=1` if only a subset of images have gravity direction. 36 | If gravity measurements are subject to i.i.d. noise, they can be refined by setting `--refine_gravity=1`. 37 | 38 | 39 | ## File Formats 40 | ### Relative Pose 41 | The relative pose file is expected to be of the following format 42 | ``` 43 | IMAGE_NAME_1 IMAGE_NAME_2 QW QX QY QZ TX TY TZ 44 | ``` 45 | Only images contained in at least one relative pose will be included in the following procedure. 46 | The relative pose should be 2R1 x1 + 2t1 = x2. 47 | 48 | ### Gravity Direction 49 | The gravity direction file is expected to be of the following format 50 | ``` 51 | IMAGE_NAME GX GY GZ 52 | ``` 53 | The gravity direction $g$ should $[0, 1, 0]$ if the image is orthogonal to the ground plane, and the estimated rotation would have the property that $R_i \cdot [0, 1, 0]^\top = g$. 54 | More explicitly, suppose we can transpose a 3D point from the world coordinate to the image coordinate by RX + t = x. Here: 55 | - `R` is a 3x3 rotation matrix that aligns the world coordinate system with the image coordinate system. 56 | - `X` is a 3D point in the world coordinate system. 57 | - `t` is a 3x1 translation vector that shifts the world coordinate system to the image coordinate system. 58 | - `x` is the corresponding 3D point in the image coordinate system. 59 | The gravity direction should be the second column of the rotation matrix `R`. 60 | 61 | It is acceptable if only a subset of all images have gravity direction. 62 | If the specified image name does not match any known image name from relative pose, it is ignored. 63 | 64 | ### Output 65 | The estimated global rotation will be in the following format 66 | ``` 67 | IMAGE_NAME QW QX QY QZ 68 | ``` 69 | Any images that are not within the largest connected component of the view-graph formed by the relative pose will be ignored. 70 | -------------------------------------------------------------------------------- /glomap/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(SOURCES 2 | controllers/global_mapper.cc 3 | controllers/option_manager.cc 4 | controllers/rotation_averager.cc 5 | controllers/track_establishment.cc 6 | controllers/track_retriangulation.cc 7 | estimators/bundle_adjustment.cc 8 | estimators/global_positioning.cc 9 | estimators/global_rotation_averaging.cc 10 | estimators/gravity_refinement.cc 11 | estimators/relpose_estimation.cc 12 | estimators/view_graph_calibration.cc 13 | io/colmap_converter.cc 14 | io/colmap_io.cc 15 | io/pose_io.cc 16 | math/gravity.cc 17 | math/rigid3d.cc 18 | math/tree.cc 19 | math/two_view_geometry.cc 20 | processors/image_pair_inliers.cc 21 | processors/image_undistorter.cc 22 | processors/reconstruction_normalizer.cc 23 | processors/reconstruction_pruning.cc 24 | processors/relpose_filter.cc 25 | processors/track_filter.cc 26 | processors/view_graph_manipulation.cc 27 | scene/view_graph.cc 28 | ) 29 | 30 | set(HEADERS 31 | controllers/global_mapper.h 32 | controllers/option_manager.h 33 | controllers/rotation_averager.h 34 | controllers/track_establishment.h 35 | controllers/track_retriangulation.h 36 | estimators/bundle_adjustment.h 37 | estimators/cost_function.h 38 | estimators/global_positioning.h 39 | estimators/global_rotation_averaging.h 40 | estimators/gravity_refinement.h 41 | estimators/relpose_estimation.h 42 | estimators/optimization_base.h 43 | estimators/view_graph_calibration.h 44 | io/colmap_converter.h 45 | io/colmap_io.h 46 | io/pose_io.h 47 | math/gravity.h 48 | math/l1_solver.h 49 | math/rigid3d.h 50 | math/tree.h 51 | math/two_view_geometry.h 52 | math/union_find.h 53 | processors/image_pair_inliers.h 54 | processors/image_undistorter.h 55 | processors/reconstruction_normalizer.h 56 | processors/reconstruction_pruning.h 57 | processors/relpose_filter.h 58 | processors/track_filter.h 59 | processors/view_graph_manipulation.h 60 | scene/camera.h 61 | scene/image_pair.h 62 | scene/image.h 63 | scene/track.h 64 | scene/types_sfm.h 65 | scene/types.h 66 | ) 67 | 68 | add_library(glomap ${SOURCES} ${HEADERS}) 69 | if(NOT FETCH_COLMAP) 70 | target_link_libraries(glomap PUBLIC colmap::colmap) 71 | else() 72 | target_link_libraries(glomap PUBLIC colmap) 73 | endif() 74 | 75 | if(NOT FETCH_POSELIB) 76 | target_link_libraries(glomap PUBLIC PoseLib::PoseLib) 77 | else() 78 | target_link_libraries(glomap PUBLIC PoseLib) 79 | endif() 80 | 81 | target_link_libraries( 82 | glomap 83 | PUBLIC 84 | Eigen3::Eigen 85 | Ceres::ceres 86 | SuiteSparse::CHOLMOD 87 | ${BOOST_LIBRARIES} 88 | ) 89 | target_include_directories(glomap PUBLIC ..) 90 | 91 | if(MSVC) 92 | target_compile_options(glomap PRIVATE /bigobj) 93 | else() 94 | target_compile_options(glomap PRIVATE 95 | -Wall 96 | -Werror 97 | -Wno-sign-compare 98 | -Wno-unused-variable 99 | ) 100 | endif() 101 | 102 | 103 | add_executable(glomap_main 104 | glomap.cc 105 | exe/global_mapper.h 106 | exe/global_mapper.cc 107 | exe/rotation_averager.h 108 | exe/rotation_averager.cc 109 | ) 110 | target_link_libraries(glomap_main glomap) 111 | 112 | set_target_properties(glomap_main PROPERTIES OUTPUT_NAME glomap) 113 | install(TARGETS glomap_main DESTINATION bin) 114 | 115 | 116 | if(TESTS_ENABLED) 117 | add_executable(glomap_test 118 | controllers/global_mapper_test.cc 119 | controllers/rotation_averager_test.cc 120 | ) 121 | target_link_libraries( 122 | glomap_test 123 | PRIVATE 124 | glomap 125 | GTest::gtest 126 | GTest::gtest_main) 127 | add_test(NAME glomap_test COMMAND glomap_test) 128 | endif() 129 | -------------------------------------------------------------------------------- /glomap/controllers/global_mapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/controllers/track_establishment.h" 4 | #include "glomap/controllers/track_retriangulation.h" 5 | #include "glomap/estimators/bundle_adjustment.h" 6 | #include "glomap/estimators/global_positioning.h" 7 | #include "glomap/estimators/global_rotation_averaging.h" 8 | #include "glomap/estimators/relpose_estimation.h" 9 | #include "glomap/estimators/view_graph_calibration.h" 10 | #include "glomap/types.h" 11 | 12 | #include 13 | 14 | namespace glomap { 15 | 16 | struct GlobalMapperOptions { 17 | // Options for each component 18 | ViewGraphCalibratorOptions opt_vgcalib; 19 | RelativePoseEstimationOptions opt_relpose; 20 | RotationEstimatorOptions opt_ra; 21 | TrackEstablishmentOptions opt_track; 22 | GlobalPositionerOptions opt_gp; 23 | BundleAdjusterOptions opt_ba; 24 | TriangulatorOptions opt_triangulator; 25 | 26 | // Inlier thresholds for each component 27 | InlierThresholdOptions inlier_thresholds; 28 | 29 | // Control the number of iterations for each component 30 | int num_iteration_bundle_adjustment = 3; 31 | int num_iteration_retriangulation = 1; 32 | 33 | // Control the flow of the global sfm 34 | bool skip_preprocessing = false; 35 | bool skip_view_graph_calibration = false; 36 | bool skip_relative_pose_estimation = false; 37 | bool skip_rotation_averaging = false; 38 | bool skip_track_establishment = false; 39 | bool skip_global_positioning = false; 40 | bool skip_bundle_adjustment = false; 41 | bool skip_retriangulation = false; 42 | bool skip_pruning = true; 43 | }; 44 | 45 | class GlobalMapper { 46 | public: 47 | GlobalMapper(const GlobalMapperOptions& options) : options_(options) {} 48 | 49 | bool Solve(const colmap::Database& database, 50 | ViewGraph& view_graph, 51 | std::unordered_map& cameras, 52 | std::unordered_map& images, 53 | std::unordered_map& tracks); 54 | 55 | private: 56 | const GlobalMapperOptions options_; 57 | }; 58 | 59 | } // namespace glomap 60 | -------------------------------------------------------------------------------- /glomap/controllers/global_mapper_test.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/controllers/global_mapper.h" 2 | 3 | #include "glomap/io/colmap_io.h" 4 | #include "glomap/types.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | namespace glomap { 13 | namespace { 14 | 15 | void ExpectEqualReconstructions(const colmap::Reconstruction& gt, 16 | const colmap::Reconstruction& computed, 17 | const double max_rotation_error_deg, 18 | const double max_proj_center_error, 19 | const double num_obs_tolerance) { 20 | EXPECT_EQ(computed.NumCameras(), gt.NumCameras()); 21 | EXPECT_EQ(computed.NumImages(), gt.NumImages()); 22 | EXPECT_EQ(computed.NumRegImages(), gt.NumRegImages()); 23 | EXPECT_GE(computed.ComputeNumObservations(), 24 | (1 - num_obs_tolerance) * gt.ComputeNumObservations()); 25 | 26 | colmap::Sim3d gtFromComputed; 27 | colmap::AlignReconstructionsViaProjCenters(computed, 28 | gt, 29 | /*max_proj_center_error=*/0.1, 30 | >FromComputed); 31 | 32 | const std::vector errors = 33 | colmap::ComputeImageAlignmentError(computed, gt, gtFromComputed); 34 | EXPECT_EQ(errors.size(), gt.NumImages()); 35 | for (const auto& error : errors) { 36 | EXPECT_LT(error.rotation_error_deg, max_rotation_error_deg); 37 | EXPECT_LT(error.proj_center_error, max_proj_center_error); 38 | } 39 | } 40 | 41 | GlobalMapperOptions CreateTestOptions() { 42 | GlobalMapperOptions options; 43 | options.skip_view_graph_calibration = false; 44 | options.skip_relative_pose_estimation = false; 45 | options.skip_rotation_averaging = false; 46 | options.skip_track_establishment = false; 47 | options.skip_global_positioning = false; 48 | options.skip_bundle_adjustment = false; 49 | options.skip_retriangulation = false; 50 | return options; 51 | } 52 | 53 | TEST(GlobalMapper, WithoutNoise) { 54 | const std::string database_path = colmap::CreateTestDir() + "/database.db"; 55 | 56 | colmap::Database database(database_path); 57 | colmap::Reconstruction gt_reconstruction; 58 | colmap::SyntheticDatasetOptions synthetic_dataset_options; 59 | synthetic_dataset_options.num_cameras = 2; 60 | synthetic_dataset_options.num_images = 7; 61 | synthetic_dataset_options.num_points3D = 50; 62 | synthetic_dataset_options.point2D_stddev = 0; 63 | colmap::SynthesizeDataset( 64 | synthetic_dataset_options, >_reconstruction, &database); 65 | 66 | ViewGraph view_graph; 67 | std::unordered_map cameras; 68 | std::unordered_map images; 69 | std::unordered_map tracks; 70 | 71 | ConvertDatabaseToGlomap(database, view_graph, cameras, images); 72 | 73 | GlobalMapper global_mapper(CreateTestOptions()); 74 | global_mapper.Solve(database, view_graph, cameras, images, tracks); 75 | 76 | colmap::Reconstruction reconstruction; 77 | ConvertGlomapToColmap(cameras, images, tracks, reconstruction); 78 | 79 | ExpectEqualReconstructions(gt_reconstruction, 80 | reconstruction, 81 | /*max_rotation_error_deg=*/1e-2, 82 | /*max_proj_center_error=*/1e-4, 83 | /*num_obs_tolerance=*/0); 84 | } 85 | 86 | TEST(GlobalMapper, WithNoiseAndOutliers) { 87 | const std::string database_path = colmap::CreateTestDir() + "/database.db"; 88 | 89 | colmap::Database database(database_path); 90 | colmap::Reconstruction gt_reconstruction; 91 | colmap::SyntheticDatasetOptions synthetic_dataset_options; 92 | synthetic_dataset_options.num_cameras = 2; 93 | synthetic_dataset_options.num_images = 7; 94 | synthetic_dataset_options.num_points3D = 100; 95 | synthetic_dataset_options.point2D_stddev = 0.5; 96 | synthetic_dataset_options.inlier_match_ratio = 0.6; 97 | colmap::SynthesizeDataset( 98 | synthetic_dataset_options, >_reconstruction, &database); 99 | 100 | ViewGraph view_graph; 101 | std::unordered_map cameras; 102 | std::unordered_map images; 103 | std::unordered_map tracks; 104 | 105 | ConvertDatabaseToGlomap(database, view_graph, cameras, images); 106 | 107 | GlobalMapper global_mapper(CreateTestOptions()); 108 | global_mapper.Solve(database, view_graph, cameras, images, tracks); 109 | 110 | colmap::Reconstruction reconstruction; 111 | ConvertGlomapToColmap(cameras, images, tracks, reconstruction); 112 | 113 | ExpectEqualReconstructions(gt_reconstruction, 114 | reconstruction, 115 | /*max_rotation_error_deg=*/1e-1, 116 | /*max_proj_center_error=*/1e-1, 117 | /*num_obs_tolerance=*/0.02); 118 | } 119 | 120 | } // namespace 121 | } // namespace glomap 122 | -------------------------------------------------------------------------------- /glomap/controllers/option_manager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | namespace glomap { 11 | 12 | struct GlobalMapperOptions; 13 | struct ViewGraphCalibratorOptions; 14 | struct RelativePoseEstimationOptions; 15 | struct RotationEstimatorOptions; 16 | struct TrackEstablishmentOptions; 17 | struct GlobalPositionerOptions; 18 | struct BundleAdjusterOptions; 19 | struct TriangulatorOptions; 20 | struct InlierThresholdOptions; 21 | struct GravityRefinerOptions; 22 | 23 | class OptionManager { 24 | public: 25 | explicit OptionManager(bool add_project_options = true); 26 | void AddAllOptions(); 27 | void AddDatabaseOptions(); 28 | void AddImageOptions(); 29 | void AddGlobalMapperOptions(); 30 | void AddGlobalMapperFullOptions(); 31 | void AddGlobalMapperResumeOptions(); 32 | void AddGlobalMapperResumeFullOptions(); 33 | void AddViewGraphCalibrationOptions(); 34 | void AddRelativePoseEstimationOptions(); 35 | void AddRotationEstimatorOptions(); 36 | void AddTrackEstablishmentOptions(); 37 | void AddGlobalPositionerOptions(); 38 | void AddBundleAdjusterOptions(); 39 | void AddTriangulatorOptions(); 40 | void AddInlierThresholdOptions(); 41 | void AddGravityRefinerOptions(); 42 | 43 | template 44 | void AddRequiredOption(const std::string& name, 45 | T* option, 46 | const std::string& help_text = ""); 47 | template 48 | void AddDefaultOption(const std::string& name, 49 | T* option, 50 | const std::string& help_text = ""); 51 | 52 | void Reset(); 53 | void ResetOptions(bool reset_paths); 54 | 55 | void Parse(int argc, char** argv); 56 | 57 | std::shared_ptr database_path; 58 | std::shared_ptr image_path; 59 | 60 | std::shared_ptr mapper; 61 | std::shared_ptr gravity_refiner; 62 | 63 | private: 64 | template 65 | void AddAndRegisterRequiredOption(const std::string& name, 66 | T* option, 67 | const std::string& help_text = ""); 68 | template 69 | void AddAndRegisterDefaultOption(const std::string& name, 70 | T* option, 71 | const std::string& help_text = ""); 72 | 73 | template 74 | void RegisterOption(const std::string& name, const T* option); 75 | 76 | std::shared_ptr desc_; 77 | 78 | std::vector> options_bool_; 79 | std::vector> options_int_; 80 | std::vector> options_double_; 81 | std::vector> options_string_; 82 | 83 | bool added_database_options_ = false; 84 | bool added_image_options_ = false; 85 | bool added_mapper_options_ = false; 86 | bool added_view_graph_calibration_options_ = false; 87 | bool added_relative_pose_options_ = false; 88 | bool added_rotation_averaging_options_ = false; 89 | bool added_track_establishment_options_ = false; 90 | bool added_global_positioning_options_ = false; 91 | bool added_bundle_adjustment_options_ = false; 92 | bool added_triangulation_options_ = false; 93 | bool added_inliers_options_ = false; 94 | bool added_gravity_refiner_options_ = false; 95 | }; 96 | 97 | template 98 | void OptionManager::AddRequiredOption(const std::string& name, 99 | T* option, 100 | const std::string& help_text) { 101 | desc_->add_options()(name.c_str(), 102 | boost::program_options::value(option)->required(), 103 | help_text.c_str()); 104 | } 105 | 106 | template 107 | void OptionManager::AddDefaultOption(const std::string& name, 108 | T* option, 109 | const std::string& help_text) { 110 | desc_->add_options()( 111 | name.c_str(), 112 | boost::program_options::value(option)->default_value(*option), 113 | help_text.c_str()); 114 | } 115 | 116 | template 117 | void OptionManager::AddAndRegisterRequiredOption(const std::string& name, 118 | T* option, 119 | const std::string& help_text) { 120 | desc_->add_options()(name.c_str(), 121 | boost::program_options::value(option)->required(), 122 | help_text.c_str()); 123 | RegisterOption(name, option); 124 | } 125 | 126 | template 127 | void OptionManager::AddAndRegisterDefaultOption(const std::string& name, 128 | T* option, 129 | const std::string& help_text) { 130 | desc_->add_options()( 131 | name.c_str(), 132 | boost::program_options::value(option)->default_value(*option), 133 | help_text.c_str()); 134 | RegisterOption(name, option); 135 | } 136 | 137 | template 138 | void OptionManager::RegisterOption(const std::string& name, const T* option) { 139 | if (std::is_same::value) { 140 | options_bool_.emplace_back(name, reinterpret_cast(option)); 141 | } else if (std::is_same::value) { 142 | options_int_.emplace_back(name, reinterpret_cast(option)); 143 | } else if (std::is_same::value) { 144 | options_double_.emplace_back(name, reinterpret_cast(option)); 145 | } else if (std::is_same::value) { 146 | options_string_.emplace_back(name, 147 | reinterpret_cast(option)); 148 | } else { 149 | LOG(ERROR) << "Unsupported option type: " << name; 150 | } 151 | } 152 | 153 | } // namespace glomap 154 | -------------------------------------------------------------------------------- /glomap/controllers/rotation_averager.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/controllers/rotation_averager.h" 2 | 3 | namespace glomap { 4 | 5 | bool SolveRotationAveraging(ViewGraph& view_graph, 6 | std::unordered_map& images, 7 | const RotationAveragerOptions& options) { 8 | view_graph.KeepLargestConnectedComponents(images); 9 | 10 | bool solve_1dof_system = options.use_gravity && options.use_stratified; 11 | 12 | ViewGraph view_graph_grav; 13 | image_pair_t total_pairs = 0; 14 | image_pair_t grav_pairs = 0; 15 | if (solve_1dof_system) { 16 | // Prepare two sets: ones all with gravity, and one does not have gravity. 17 | // Solve them separately first, then solve them in a single system 18 | for (const auto& [pair_id, image_pair] : view_graph.image_pairs) { 19 | if (!image_pair.is_valid) continue; 20 | 21 | image_t image_id1 = image_pair.image_id1; 22 | image_t image_id2 = image_pair.image_id2; 23 | 24 | Image& image1 = images[image_id1]; 25 | Image& image2 = images[image_id2]; 26 | 27 | if (!image1.is_registered || !image2.is_registered) continue; 28 | 29 | total_pairs++; 30 | 31 | if (image1.gravity_info.has_gravity && image2.gravity_info.has_gravity) { 32 | view_graph_grav.image_pairs.emplace( 33 | pair_id, 34 | ImagePair(image_id1, image_id2, image_pair.cam2_from_cam1)); 35 | grav_pairs++; 36 | } 37 | } 38 | } 39 | 40 | // If there is no image pairs with gravity or most image pairs are with 41 | // gravity, then just run the 3dof version 42 | bool status = (grav_pairs == 0) || (grav_pairs > total_pairs * 0.95); 43 | solve_1dof_system = solve_1dof_system && (!status); 44 | 45 | if (solve_1dof_system) { 46 | // Run the 1dof optimization 47 | LOG(INFO) << "Solving subset 1DoF rotation averaging problem in the mixed " 48 | "prior system"; 49 | int num_img_grv = view_graph_grav.KeepLargestConnectedComponents(images); 50 | RotationEstimator rotation_estimator_grav(options); 51 | if (!rotation_estimator_grav.EstimateRotations(view_graph_grav, images)) { 52 | return false; 53 | } 54 | view_graph.KeepLargestConnectedComponents(images); 55 | } 56 | 57 | RotationEstimator rotation_estimator(options); 58 | return rotation_estimator.EstimateRotations(view_graph, images); 59 | } 60 | 61 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/controllers/rotation_averager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/estimators/global_rotation_averaging.h" 4 | 5 | namespace glomap { 6 | 7 | struct RotationAveragerOptions : public RotationEstimatorOptions { 8 | bool use_stratified = true; 9 | }; 10 | 11 | bool SolveRotationAveraging(ViewGraph& view_graph, 12 | std::unordered_map& images, 13 | const RotationAveragerOptions& options); 14 | 15 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/controllers/track_establishment.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "glomap/math/union_find.h" 5 | #include "glomap/scene/types_sfm.h" 6 | 7 | namespace glomap { 8 | 9 | struct TrackEstablishmentOptions { 10 | // the max allowed distance for features in the same track in the same image 11 | double thres_inconsistency = 10.; 12 | 13 | // The minimal number of tracks for each view, 14 | int min_num_tracks_per_view = -1; 15 | 16 | // The minimal number of tracks for each view pair 17 | int min_num_view_per_track = 3; 18 | 19 | // The maximal number of tracks for each view pair 20 | int max_num_view_per_track = 100; 21 | 22 | // The maximal number of tracks 23 | int max_num_tracks = 10000000; 24 | }; 25 | 26 | class TrackEngine { 27 | public: 28 | TrackEngine(const ViewGraph& view_graph, 29 | const std::unordered_map& images, 30 | const TrackEstablishmentOptions& options) 31 | : options_(options), view_graph_(view_graph), images_(images) {} 32 | 33 | // Establish tracks from the view graph. Exclude the tracks that are not 34 | // consistent Return the number of tracks 35 | size_t EstablishFullTracks(std::unordered_map& tracks); 36 | 37 | // Subsample the tracks, and exclude too short / long tracks 38 | // Return the number of tracks 39 | size_t FindTracksForProblem( 40 | const std::unordered_map& tracks_full, 41 | std::unordered_map& tracks_selected); 42 | 43 | private: 44 | // Blindly concatenate tracks if any matches occur 45 | void BlindConcatenation(); 46 | 47 | // Iterate through the collected tracks and record the items for each track 48 | void TrackCollection(std::unordered_map& tracks); 49 | 50 | const TrackEstablishmentOptions& options_; 51 | 52 | const ViewGraph& view_graph_; 53 | const std::unordered_map& images_; 54 | 55 | // Internal structure used for concatenating tracks 56 | UnionFind uf_; 57 | }; 58 | 59 | } // namespace glomap 60 | -------------------------------------------------------------------------------- /glomap/controllers/track_retriangulation.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/controllers/track_retriangulation.h" 2 | 3 | #include "glomap/io/colmap_converter.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | namespace glomap { 12 | 13 | bool RetriangulateTracks(const TriangulatorOptions& options, 14 | const colmap::Database& database, 15 | std::unordered_map& cameras, 16 | std::unordered_map& images, 17 | std::unordered_map& tracks) { 18 | // Following code adapted from COLMAP 19 | auto database_cache = 20 | colmap::DatabaseCache::Create(database, 21 | options.min_num_matches, 22 | false, // ignore_watermarks 23 | {} // reconstruct all possible images 24 | ); 25 | 26 | // Check whether the image is in the database cache. If not, set the image 27 | // as not registered to avoid memory error. 28 | std::vector image_ids_notconnected; 29 | for (auto& image : images) { 30 | if (!database_cache->ExistsImage(image.first) && 31 | image.second.is_registered) { 32 | image.second.is_registered = false; 33 | image_ids_notconnected.push_back(image.first); 34 | } 35 | } 36 | 37 | // Convert the glomap data structures to colmap data structures 38 | std::shared_ptr reconstruction_ptr = 39 | std::make_shared(); 40 | ConvertGlomapToColmap(cameras, 41 | images, 42 | std::unordered_map(), 43 | *reconstruction_ptr); 44 | 45 | colmap::IncrementalPipelineOptions options_colmap; 46 | options_colmap.triangulation.complete_max_reproj_error = 47 | options.tri_complete_max_reproj_error; 48 | options_colmap.triangulation.merge_max_reproj_error = 49 | options.tri_merge_max_reproj_error; 50 | options_colmap.triangulation.min_angle = options.tri_min_angle; 51 | 52 | reconstruction_ptr->DeleteAllPoints2DAndPoints3D(); 53 | reconstruction_ptr->TranscribeImageIdsToDatabase(database); 54 | 55 | colmap::IncrementalMapper mapper(database_cache); 56 | mapper.BeginReconstruction(reconstruction_ptr); 57 | 58 | // Triangulate all images. 59 | const auto tri_options = options_colmap.Triangulation(); 60 | const auto mapper_options = options_colmap.Mapper(); 61 | 62 | const std::set& reg_image_ids = reconstruction_ptr->RegImageIds(); 63 | 64 | size_t image_idx = 0; 65 | for (const image_t image_id : reg_image_ids) { 66 | std::cout << "\r Triangulating image " << image_idx++ + 1 << " / " 67 | << reg_image_ids.size() << std::flush; 68 | 69 | const auto& image = reconstruction_ptr->Image(image_id); 70 | 71 | int num_tris = mapper.TriangulateImage(tri_options, image_id); 72 | } 73 | std::cout << std::endl; 74 | 75 | // Merge and complete tracks. 76 | mapper.CompleteAndMergeTracks(tri_options); 77 | 78 | auto ba_options = options_colmap.GlobalBundleAdjustment(); 79 | ba_options.refine_focal_length = false; 80 | ba_options.refine_principal_point = false; 81 | ba_options.refine_extra_params = false; 82 | ba_options.refine_extrinsics = false; 83 | 84 | // Configure bundle adjustment. 85 | colmap::BundleAdjustmentConfig ba_config; 86 | for (const image_t image_id : reg_image_ids) { 87 | ba_config.AddImage(image_id); 88 | } 89 | 90 | colmap::ObservationManager observation_manager(*reconstruction_ptr); 91 | 92 | for (int i = 0; i < options_colmap.ba_global_max_refinements; ++i) { 93 | std::cout << "\r Global bundle adjustment iteration " << i + 1 << " / " 94 | << options_colmap.ba_global_max_refinements << std::flush; 95 | // Avoid degeneracies in bundle adjustment. 96 | observation_manager.FilterObservationsWithNegativeDepth(); 97 | 98 | const size_t num_observations = 99 | reconstruction_ptr->ComputeNumObservations(); 100 | 101 | std::unique_ptr bundle_adjuster; 102 | bundle_adjuster = 103 | CreateDefaultBundleAdjuster(ba_options, ba_config, *reconstruction_ptr); 104 | if (bundle_adjuster->Solve().termination_type == ceres::FAILURE) { 105 | return false; 106 | } 107 | 108 | size_t num_changed_observations = 0; 109 | num_changed_observations += mapper.CompleteAndMergeTracks(tri_options); 110 | num_changed_observations += mapper.FilterPoints(mapper_options); 111 | const double changed = 112 | static_cast(num_changed_observations) / num_observations; 113 | if (changed < options_colmap.ba_global_max_refinement_change) { 114 | break; 115 | } 116 | } 117 | std::cout << std::endl; 118 | 119 | // Add the removed images to the reconstruction 120 | for (const auto& image_id : image_ids_notconnected) { 121 | images[image_id].is_registered = true; 122 | colmap::Image image_colmap; 123 | ConvertGlomapToColmapImage(images[image_id], image_colmap, true); 124 | reconstruction_ptr->AddImage(std::move(image_colmap)); 125 | } 126 | 127 | // Convert the colmap data structures back to glomap data structures 128 | ConvertColmapToGlomap(*reconstruction_ptr, cameras, images, tracks); 129 | 130 | return true; 131 | } 132 | 133 | } // namespace glomap 134 | -------------------------------------------------------------------------------- /glomap/controllers/track_retriangulation.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "glomap/scene/types_sfm.h" 5 | 6 | #include 7 | 8 | namespace glomap { 9 | 10 | struct TriangulatorOptions { 11 | double tri_complete_max_reproj_error = 15.0; 12 | double tri_merge_max_reproj_error = 15.0; 13 | double tri_min_angle = 1.0; 14 | 15 | int min_num_matches = 15; 16 | }; 17 | 18 | bool RetriangulateTracks(const TriangulatorOptions& options, 19 | const colmap::Database& database, 20 | std::unordered_map& cameras, 21 | std::unordered_map& images, 22 | std::unordered_map& tracks); 23 | 24 | } // namespace glomap 25 | -------------------------------------------------------------------------------- /glomap/estimators/bundle_adjustment.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/estimators/optimization_base.h" 4 | #include "glomap/scene/types_sfm.h" 5 | #include "glomap/types.h" 6 | 7 | #include 8 | 9 | namespace glomap { 10 | 11 | struct BundleAdjusterOptions : public OptimizationBaseOptions { 12 | public: 13 | // Flags for which parameters to optimize 14 | bool optimize_rotations = true; 15 | bool optimize_translation = true; 16 | bool optimize_intrinsics = true; 17 | bool optimize_principal_point = false; 18 | bool optimize_points = true; 19 | 20 | bool use_gpu = true; 21 | std::string gpu_index = "-1"; 22 | int min_num_images_gpu_solver = 50; 23 | 24 | // Constrain the minimum number of views per track 25 | int min_num_view_per_track = 3; 26 | 27 | BundleAdjusterOptions() : OptimizationBaseOptions() { 28 | thres_loss_function = 1.; 29 | solver_options.max_num_iterations = 200; 30 | } 31 | 32 | std::shared_ptr CreateLossFunction() { 33 | return std::make_shared(thres_loss_function); 34 | } 35 | }; 36 | 37 | class BundleAdjuster { 38 | public: 39 | BundleAdjuster(const BundleAdjusterOptions& options) : options_(options) {} 40 | 41 | // Returns true if the optimization was a success, false if there was a 42 | // failure. 43 | // Assume tracks here are already filtered 44 | bool Solve(const ViewGraph& view_graph, 45 | std::unordered_map& cameras, 46 | std::unordered_map& images, 47 | std::unordered_map& tracks); 48 | 49 | BundleAdjusterOptions& GetOptions() { return options_; } 50 | 51 | private: 52 | // Reset the problem 53 | void Reset(); 54 | 55 | // Add tracks to the problem 56 | void AddPointToCameraConstraints( 57 | const ViewGraph& view_graph, 58 | std::unordered_map& cameras, 59 | std::unordered_map& images, 60 | std::unordered_map& tracks); 61 | 62 | // Set the parameter groups 63 | void AddCamerasAndPointsToParameterGroups( 64 | std::unordered_map& cameras, 65 | std::unordered_map& images, 66 | std::unordered_map& tracks); 67 | 68 | // Parameterize the variables, set some variables to be constant if desired 69 | void ParameterizeVariables(std::unordered_map& cameras, 70 | std::unordered_map& images, 71 | std::unordered_map& tracks); 72 | 73 | BundleAdjusterOptions options_; 74 | 75 | std::unique_ptr problem_; 76 | std::shared_ptr loss_function_; 77 | }; 78 | 79 | } // namespace glomap 80 | -------------------------------------------------------------------------------- /glomap/estimators/global_positioning.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/estimators/optimization_base.h" 4 | #include "glomap/scene/types_sfm.h" 5 | #include "glomap/types.h" 6 | 7 | namespace glomap { 8 | 9 | struct GlobalPositionerOptions : public OptimizationBaseOptions { 10 | // ONLY_POINTS is recommended 11 | enum ConstraintType { 12 | // only include camera to point constraints 13 | ONLY_POINTS, 14 | // only include camera to camera constraints 15 | ONLY_CAMERAS, 16 | // the points and cameras are reweighted to have similar total contribution 17 | POINTS_AND_CAMERAS_BALANCED, 18 | // treat each contribution from camera to point and camera to camera equally 19 | POINTS_AND_CAMERAS, 20 | }; 21 | 22 | // Whether initialize the reconstruction randomly 23 | bool generate_random_positions = true; 24 | bool generate_random_points = true; 25 | bool generate_scales = true; // Now using fixed 1 as initializaiton 26 | 27 | // Flags for which parameters to optimize 28 | bool optimize_positions = true; 29 | bool optimize_points = true; 30 | bool optimize_scales = true; 31 | 32 | bool use_gpu = true; 33 | std::string gpu_index = "-1"; 34 | int min_num_images_gpu_solver = 50; 35 | 36 | // Constrain the minimum number of views per track 37 | int min_num_view_per_track = 3; 38 | 39 | // Random seed 40 | unsigned seed = 1; 41 | 42 | // the type of global positioning 43 | ConstraintType constraint_type = ONLY_POINTS; 44 | double constraint_reweight_scale = 45 | 1.0; // only relevant for POINTS_AND_CAMERAS_BALANCED 46 | 47 | GlobalPositionerOptions() : OptimizationBaseOptions() { 48 | thres_loss_function = 1e-1; 49 | } 50 | 51 | std::shared_ptr CreateLossFunction() { 52 | return std::make_shared(thres_loss_function); 53 | } 54 | }; 55 | 56 | class GlobalPositioner { 57 | public: 58 | GlobalPositioner(const GlobalPositionerOptions& options); 59 | 60 | // Returns true if the optimization was a success, false if there was a 61 | // failure. 62 | // Assume tracks here are already filtered 63 | bool Solve(const ViewGraph& view_graph, 64 | std::unordered_map& cameras, 65 | std::unordered_map& images, 66 | std::unordered_map& tracks); 67 | 68 | GlobalPositionerOptions& GetOptions() { return options_; } 69 | 70 | protected: 71 | void SetupProblem(const ViewGraph& view_graph, 72 | const std::unordered_map& tracks); 73 | 74 | // Initialize all cameras to be random. 75 | void InitializeRandomPositions(const ViewGraph& view_graph, 76 | std::unordered_map& images, 77 | std::unordered_map& tracks); 78 | 79 | // Creates camera to camera constraints from relative translations. (3D) 80 | void AddCameraToCameraConstraints(const ViewGraph& view_graph, 81 | std::unordered_map& images); 82 | 83 | // Add tracks to the problem 84 | void AddPointToCameraConstraints( 85 | std::unordered_map& cameras, 86 | std::unordered_map& images, 87 | std::unordered_map& tracks); 88 | 89 | // Add a single track to the problem 90 | void AddTrackToProblem(track_t track_id, 91 | std::unordered_map& cameras, 92 | std::unordered_map& images, 93 | std::unordered_map& tracks); 94 | 95 | // Set the parameter groups 96 | void AddCamerasAndPointsToParameterGroups( 97 | std::unordered_map& images, 98 | std::unordered_map& tracks); 99 | 100 | // Parameterize the variables, set some variables to be constant if desired 101 | void ParameterizeVariables(std::unordered_map& images, 102 | std::unordered_map& tracks); 103 | 104 | // During the optimization, the camera translation is set to be the camera 105 | // center Convert the results back to camera poses 106 | void ConvertResults(std::unordered_map& images); 107 | 108 | GlobalPositionerOptions options_; 109 | 110 | std::mt19937 random_generator_; 111 | std::unique_ptr problem_; 112 | 113 | // Loss functions for reweighted terms. 114 | std::shared_ptr loss_function_; 115 | std::shared_ptr loss_function_ptcam_uncalibrated_; 116 | std::shared_ptr loss_function_ptcam_calibrated_; 117 | 118 | // Auxiliary scale variables. 119 | std::vector scales_; 120 | }; 121 | 122 | } // namespace glomap 123 | -------------------------------------------------------------------------------- /glomap/estimators/global_rotation_averaging.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/math/l1_solver.h" 4 | #include "glomap/scene/types_sfm.h" 5 | #include "glomap/types.h" 6 | 7 | #include 8 | #include 9 | 10 | // Code is adapted from Theia's RobustRotationEstimator 11 | // (http://www.theia-sfm.org/). For gravity aligned rotation averaging, refere 12 | // to the paper "Gravity Aligned Rotation Averaging" 13 | namespace glomap { 14 | 15 | // The struct to store the temporary information for each image pair 16 | struct ImagePairTempInfo { 17 | // The index of relative pose in the residual vector 18 | image_pair_t index = -1; 19 | 20 | // Whether the relative rotation is gravity aligned 21 | double has_gravity = false; 22 | 23 | // The relative rotation between the two images (x, z component) 24 | double xz_error = 0; 25 | 26 | // R_rel is gravity aligned if gravity prior is available, otherwise it is the 27 | // relative rotation between the two images 28 | Eigen::Matrix3d R_rel = Eigen::Matrix3d::Identity(); 29 | 30 | // angle_rel is the converted angle if gravity prior is available for both 31 | // images 32 | double angle_rel = 0; 33 | }; 34 | 35 | struct RotationEstimatorOptions { 36 | // Maximum number of times to run L1 minimization. 37 | int max_num_l1_iterations = 5; 38 | 39 | // Average step size threshold to terminate the L1 minimization 40 | double l1_step_convergence_threshold = 0.001; 41 | 42 | // The number of iterative reweighted least squares iterations to perform. 43 | int max_num_irls_iterations = 100; 44 | 45 | // Average step size threshold to termininate the IRLS minimization 46 | double irls_step_convergence_threshold = 0.001; 47 | 48 | Eigen::Vector3d axis = Eigen::Vector3d(0, 1, 0); 49 | 50 | // This is the point where the Huber-like cost function switches from L1 to 51 | // L2. 52 | double irls_loss_parameter_sigma = 5.0; // in degree 53 | 54 | enum WeightType { 55 | // For Geman-McClure weight, refer to the paper "Efficient and robust 56 | // large-scale rotation averaging" (Chatterjee et. al, 2013) 57 | GEMAN_MCCLURE, 58 | // For Half Norm, refer to the paper "Robust Relative Rotation Averaging" 59 | // (Chatterjee et. al, 2017) 60 | HALF_NORM, 61 | } weight_type = GEMAN_MCCLURE; 62 | 63 | // Flg to use maximum spanning tree for initialization 64 | bool skip_initialization = false; 65 | 66 | // Flag to use weighting for rotation averaging 67 | bool use_weight = false; 68 | 69 | // Flag to use gravity for rotation averaging 70 | bool use_gravity = false; 71 | }; 72 | 73 | // TODO: Implement the stratified camera rotation estimation 74 | // TODO: Implement the HALF_NORM loss for IRLS 75 | // TODO: Implement the weighted version for rotation averaging 76 | // TODO: Implement the gravity as prior for rotation averaging 77 | class RotationEstimator { 78 | public: 79 | explicit RotationEstimator(const RotationEstimatorOptions& options) 80 | : options_(options) {} 81 | 82 | // Estimates the global orientations of all views based on an initial 83 | // guess. Returns true on successful estimation and false otherwise. 84 | bool EstimateRotations(const ViewGraph& view_graph, 85 | std::unordered_map& images); 86 | 87 | protected: 88 | // Initialize the rotation from the maximum spanning tree 89 | // Number of inliers serve as weights 90 | void InitializeFromMaximumSpanningTree( 91 | const ViewGraph& view_graph, std::unordered_map& images); 92 | 93 | // Sets up the sparse linear system such that dR_ij = dR_j - dR_i. This is the 94 | // first-order approximation of the angle-axis rotations. This should only be 95 | // called once. 96 | void SetupLinearSystem(const ViewGraph& view_graph, 97 | std::unordered_map& images); 98 | 99 | // Performs the L1 robust loss minimization. 100 | bool SolveL1Regression(const ViewGraph& view_graph, 101 | std::unordered_map& images); 102 | 103 | // Performs the iteratively reweighted least squares. 104 | bool SolveIRLS(const ViewGraph& view_graph, 105 | std::unordered_map& images); 106 | 107 | // Updates the global rotations based on the current rotation change. 108 | void UpdateGlobalRotations(const ViewGraph& view_graph, 109 | std::unordered_map& images); 110 | 111 | // Computes the relative rotation (tangent space) residuals based on the 112 | // current global orientation estimates. 113 | void ComputeResiduals(const ViewGraph& view_graph, 114 | std::unordered_map& images); 115 | 116 | // Computes the average size of the most recent step of the algorithm. 117 | // The is the average over all non-fixed global_orientations_ of their 118 | // rotation magnitudes. 119 | double ComputeAverageStepSize( 120 | const std::unordered_map& images); 121 | 122 | // Data 123 | // Options for the solver. 124 | const RotationEstimatorOptions& options_; 125 | 126 | // The sparse matrix used to maintain the linear system. This is matrix A in 127 | // Ax = b. 128 | Eigen::SparseMatrix sparse_matrix_; 129 | 130 | // x in the linear system Ax = b. 131 | Eigen::VectorXd tangent_space_step_; 132 | 133 | // b in the linear system Ax = b. 134 | Eigen::VectorXd tangent_space_residual_; 135 | 136 | Eigen::VectorXd rotation_estimated_; 137 | 138 | // Varaibles for intermidiate results 139 | std::unordered_map image_id_to_idx_; 140 | std::unordered_map rel_temp_info_; 141 | 142 | // The fixed camera id. This is used to remove the ambiguity of the linear 143 | image_t fixed_camera_id_ = -1; 144 | 145 | // The fixed camera rotation (if with initialization, it would not be identity 146 | // matrix) 147 | Eigen::Vector3d fixed_camera_rotation_; 148 | 149 | // The weights for the edges 150 | Eigen::ArrayXd weights_; 151 | }; 152 | 153 | } // namespace glomap 154 | -------------------------------------------------------------------------------- /glomap/estimators/gravity_refinement.cc: -------------------------------------------------------------------------------- 1 | #include "gravity_refinement.h" 2 | 3 | #include "glomap/estimators/cost_function.h" 4 | #include "glomap/math/gravity.h" 5 | 6 | #include 7 | 8 | namespace glomap { 9 | void GravityRefiner::RefineGravity(const ViewGraph& view_graph, 10 | std::unordered_map& images) { 11 | const std::unordered_map& image_pairs = 12 | view_graph.image_pairs; 13 | const std::unordered_map>& 14 | adjacency_list = view_graph.GetAdjacencyList(); 15 | if (adjacency_list.empty()) { 16 | LOG(INFO) << "Adjacency list not established" << std::endl; 17 | return; 18 | } 19 | 20 | // Identify the images that are error prone 21 | int counter_rect = 0; 22 | std::unordered_set error_prone_images; 23 | IdentifyErrorProneGravity(view_graph, images, error_prone_images); 24 | 25 | if (error_prone_images.empty()) { 26 | LOG(INFO) << "No error prone images found" << std::endl; 27 | return; 28 | } 29 | 30 | loss_function_ = options_.CreateLossFunction(); 31 | 32 | int counter_progress = 0; 33 | // Iterate through the error prone images 34 | for (auto image_id : error_prone_images) { 35 | if ((counter_progress + 1) % 10 == 0 || 36 | counter_progress == error_prone_images.size() - 1) { 37 | std::cout << "\r Refining image " << counter_progress + 1 << " / " 38 | << error_prone_images.size() << std::flush; 39 | } 40 | counter_progress++; 41 | const std::unordered_set& neighbors = adjacency_list.at(image_id); 42 | std::vector gravities; 43 | gravities.reserve(neighbors.size()); 44 | 45 | ceres::Problem::Options problem_options; 46 | problem_options.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP; 47 | ceres::Problem problem(problem_options); 48 | int counter = 0; 49 | Eigen::Vector3d gravity = images[image_id].gravity_info.GetGravity(); 50 | for (const auto& neighbor : neighbors) { 51 | image_pair_t pair_id = ImagePair::ImagePairToPairId(image_id, neighbor); 52 | 53 | image_t image_id1 = image_pairs.at(pair_id).image_id1; 54 | image_t image_id2 = image_pairs.at(pair_id).image_id2; 55 | if (images.at(image_id1).gravity_info.has_gravity == false || 56 | images.at(image_id2).gravity_info.has_gravity == false) 57 | continue; 58 | 59 | if (image_id1 == image_id) { 60 | gravities.emplace_back((image_pairs.at(pair_id) 61 | .cam2_from_cam1.rotation.toRotationMatrix() 62 | .transpose() * 63 | images[image_id2].gravity_info.GetRAlign()) 64 | .col(1)); 65 | } else { 66 | gravities.emplace_back( 67 | (image_pairs.at(pair_id) 68 | .cam2_from_cam1.rotation.toRotationMatrix() * 69 | images[image_id1].gravity_info.GetRAlign()) 70 | .col(1)); 71 | } 72 | 73 | ceres::CostFunction* coor_cost = 74 | GravError::CreateCost(gravities[counter]); 75 | problem.AddResidualBlock(coor_cost, loss_function_.get(), gravity.data()); 76 | counter++; 77 | } 78 | 79 | if (gravities.size() < options_.min_num_neighbors) continue; 80 | 81 | // Then, run refinment 82 | gravity = AverageGravity(gravities); 83 | colmap::SetSphereManifold<3>(&problem, gravity.data()); 84 | ceres::Solver::Summary summary_solver; 85 | ceres::Solve(options_.solver_options, &problem, &summary_solver); 86 | 87 | // Check the error with respect to the neighbors 88 | int counter_outlier = 0; 89 | for (int i = 0; i < gravities.size(); i++) { 90 | double error = RadToDeg( 91 | std::acos(std::max(std::min(gravities[i].dot(gravity), 1.), -1.))); 92 | if (error > options_.max_gravity_error * 2) counter_outlier++; 93 | } 94 | // If the refined gravity now consistent with more images, then accept it 95 | if (double(counter_outlier) / double(gravities.size()) < 96 | options_.max_outlier_ratio) { 97 | counter_rect++; 98 | images[image_id].gravity_info.SetGravity(gravity); 99 | } 100 | } 101 | std::cout << std::endl; 102 | LOG(INFO) << "Number of rectified images: " << counter_rect << " / " 103 | << error_prone_images.size() << std::endl; 104 | } 105 | 106 | void GravityRefiner::IdentifyErrorProneGravity( 107 | const ViewGraph& view_graph, 108 | const std::unordered_map& images, 109 | std::unordered_set& error_prone_images) { 110 | error_prone_images.clear(); 111 | 112 | // image_id: (mistake, total) 113 | std::unordered_map> image_counter; 114 | // Set the counter of all images to 0 115 | for (const auto& [image_id, image] : images) { 116 | image_counter[image_id] = std::make_pair(0, 0); 117 | } 118 | 119 | for (const auto& [pair_id, image_pair] : view_graph.image_pairs) { 120 | if (!image_pair.is_valid) continue; 121 | const auto& image1 = images.at(image_pair.image_id1); 122 | const auto& image2 = images.at(image_pair.image_id2); 123 | if (image1.gravity_info.has_gravity && image2.gravity_info.has_gravity) { 124 | // Calculate the gravity aligned relative rotation 125 | const Eigen::Matrix3d R_rel = 126 | image2.gravity_info.GetRAlign().transpose() * 127 | image_pair.cam2_from_cam1.rotation.toRotationMatrix() * 128 | image1.gravity_info.GetRAlign(); 129 | // Convert it to the closest upright rotation 130 | const Eigen::Matrix3d R_rel_up = AngleToRotUp(RotUpToAngle(R_rel)); 131 | 132 | const double angle = CalcAngle(R_rel, R_rel_up); 133 | 134 | // increment the total count 135 | image_counter[image_pair.image_id1].second++; 136 | image_counter[image_pair.image_id2].second++; 137 | 138 | // increment the mistake count 139 | if (angle > options_.max_gravity_error) { 140 | image_counter[image_pair.image_id1].first++; 141 | image_counter[image_pair.image_id2].first++; 142 | } 143 | } 144 | } 145 | 146 | const std::unordered_map>& 147 | adjacency_list = view_graph.GetAdjacencyList(); 148 | 149 | // Filter the images with too many mistakes 150 | for (auto& [image_id, counter] : image_counter) { 151 | if (images.at(image_id).gravity_info.has_gravity == false) continue; 152 | if (counter.second < options_.min_num_neighbors) continue; 153 | if (double(counter.first) / double(counter.second) >= 154 | options_.max_outlier_ratio) { 155 | error_prone_images.insert(image_id); 156 | } 157 | } 158 | LOG(INFO) << "Number of error prone images: " << error_prone_images.size() 159 | << std::endl; 160 | } 161 | } // namespace glomap 162 | -------------------------------------------------------------------------------- /glomap/estimators/gravity_refinement.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/estimators/optimization_base.h" 4 | #include "glomap/math/rigid3d.h" 5 | #include "glomap/scene/types_sfm.h" 6 | #include "glomap/types.h" 7 | 8 | #include 9 | 10 | namespace glomap { 11 | 12 | struct GravityRefinerOptions : public OptimizationBaseOptions { 13 | // The minimal ratio that the gravity vector should be consistent with 14 | double max_outlier_ratio = 0.5; 15 | // The maximum allowed angle error in degree 16 | double max_gravity_error = 1.; 17 | // Only refine the gravity of the images with more than min_neighbors 18 | int min_num_neighbors = 7; 19 | 20 | GravityRefinerOptions() : OptimizationBaseOptions() {} 21 | 22 | std::shared_ptr CreateLossFunction() { 23 | return std::make_shared( 24 | 1 - std::cos(DegToRad(max_gravity_error))); 25 | } 26 | }; 27 | 28 | class GravityRefiner { 29 | public: 30 | GravityRefiner(const GravityRefinerOptions& options) : options_(options) {} 31 | void RefineGravity(const ViewGraph& view_graph, 32 | std::unordered_map& images); 33 | 34 | private: 35 | void IdentifyErrorProneGravity( 36 | const ViewGraph& view_graph, 37 | const std::unordered_map& images, 38 | std::unordered_set& error_prone_images); 39 | GravityRefinerOptions options_; 40 | std::shared_ptr loss_function_; 41 | }; 42 | 43 | } // namespace glomap 44 | -------------------------------------------------------------------------------- /glomap/estimators/optimization_base.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace glomap { 10 | 11 | struct OptimizationBaseOptions { 12 | // The threshold for the loss function 13 | double thres_loss_function = 1e-1; 14 | 15 | // The options for the solver 16 | ceres::Solver::Options solver_options; 17 | 18 | OptimizationBaseOptions() { 19 | solver_options.num_threads = std::thread::hardware_concurrency(); 20 | solver_options.max_num_iterations = 100; 21 | solver_options.minimizer_progress_to_stdout = false; 22 | solver_options.function_tolerance = 1e-5; 23 | } 24 | }; 25 | 26 | } // namespace glomap 27 | -------------------------------------------------------------------------------- /glomap/estimators/relpose_estimation.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/estimators/relpose_estimation.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace glomap { 8 | 9 | void EstimateRelativePoses(ViewGraph& view_graph, 10 | std::unordered_map& cameras, 11 | std::unordered_map& images, 12 | const RelativePoseEstimationOptions& options) { 13 | std::vector valid_pair_ids; 14 | for (auto& [image_pair_id, image_pair] : view_graph.image_pairs) { 15 | if (!image_pair.is_valid) continue; 16 | valid_pair_ids.push_back(image_pair_id); 17 | } 18 | 19 | const int64_t num_image_pairs = valid_pair_ids.size(); 20 | const int64_t kNumChunks = 10; 21 | const int64_t interval = 22 | std::ceil(static_cast(num_image_pairs) / kNumChunks); 23 | 24 | colmap::ThreadPool thread_pool(colmap::ThreadPool::kMaxNumThreads); 25 | 26 | LOG(INFO) << "Estimating relative pose for " << num_image_pairs << " pairs"; 27 | for (int64_t chunk_id = 0; chunk_id < kNumChunks; chunk_id++) { 28 | std::cout << "\r Estimating relative pose: " << chunk_id * kNumChunks << "%" 29 | << std::flush; 30 | const int64_t start = chunk_id * interval; 31 | const int64_t end = 32 | std::min((chunk_id + 1) * interval, num_image_pairs); 33 | 34 | for (int64_t pair_idx = start; pair_idx < end; pair_idx++) { 35 | thread_pool.AddTask([&, pair_idx]() { 36 | // Define as thread-local to reuse memory allocation in different tasks. 37 | thread_local std::vector points2D_1; 38 | thread_local std::vector points2D_2; 39 | thread_local std::vector inliers; 40 | 41 | ImagePair& image_pair = 42 | view_graph.image_pairs[valid_pair_ids[pair_idx]]; 43 | const Image& image1 = images[image_pair.image_id1]; 44 | const Image& image2 = images[image_pair.image_id2]; 45 | const Eigen::MatrixXi& matches = image_pair.matches; 46 | 47 | const Camera& camera1 = cameras[image1.camera_id]; 48 | const Camera& camera2 = cameras[image2.camera_id]; 49 | poselib::Camera camera_poselib1 = ColmapCameraToPoseLibCamera(camera1); 50 | poselib::Camera camera_poselib2 = ColmapCameraToPoseLibCamera(camera2); 51 | bool valid_camera_model = 52 | (camera_poselib1.model_id >= 0 && camera_poselib2.model_id >= 0); 53 | 54 | // Collect the original 2D points 55 | points2D_1.clear(); 56 | points2D_2.clear(); 57 | for (size_t idx = 0; idx < matches.rows(); idx++) { 58 | points2D_1.push_back(image1.features[matches(idx, 0)]); 59 | points2D_2.push_back(image2.features[matches(idx, 1)]); 60 | } 61 | // If the camera model is not supported by poselib 62 | if (!valid_camera_model) { 63 | // Undistort points 64 | // Note that here, we still rescale by the focal length (to avoid 65 | // change the RANSAC threshold) 66 | Eigen::Matrix2d K1_new = Eigen::Matrix2d::Zero(); 67 | Eigen::Matrix2d K2_new = Eigen::Matrix2d::Zero(); 68 | K1_new(0, 0) = camera1.FocalLengthX(); 69 | K1_new(1, 1) = camera1.FocalLengthY(); 70 | K2_new(0, 0) = camera2.FocalLengthX(); 71 | K2_new(1, 1) = camera2.FocalLengthY(); 72 | for (size_t idx = 0; idx < matches.rows(); idx++) { 73 | points2D_1[idx] = K1_new * camera1.CamFromImg(points2D_1[idx]); 74 | points2D_2[idx] = K2_new * camera2.CamFromImg(points2D_2[idx]); 75 | } 76 | 77 | // Reset the camera to be the pinhole camera with original focal 78 | // length and zero principal point 79 | camera_poselib1 = poselib::Camera( 80 | "PINHOLE", 81 | {camera1.FocalLengthX(), camera1.FocalLengthY(), 0., 0.}, 82 | camera1.width, 83 | camera1.height); 84 | camera_poselib2 = poselib::Camera( 85 | "PINHOLE", 86 | {camera2.FocalLengthX(), camera2.FocalLengthY(), 0., 0.}, 87 | camera2.width, 88 | camera2.height); 89 | } 90 | inliers.clear(); 91 | poselib::CameraPose pose_rel_calc; 92 | try { 93 | poselib::estimate_relative_pose(points2D_1, 94 | points2D_2, 95 | camera_poselib1, 96 | camera_poselib2, 97 | options.ransac_options, 98 | options.bundle_options, 99 | &pose_rel_calc, 100 | &inliers); 101 | } catch (const std::exception& e) { 102 | LOG(ERROR) << "Error in relative pose estimation: " << e.what(); 103 | image_pair.is_valid = false; 104 | return; 105 | } 106 | 107 | // Convert the relative pose to the glomap format 108 | for (int i = 0; i < 4; i++) { 109 | image_pair.cam2_from_cam1.rotation.coeffs()[i] = 110 | pose_rel_calc.q[(i + 1) % 4]; 111 | } 112 | image_pair.cam2_from_cam1.translation = pose_rel_calc.t; 113 | }); 114 | } 115 | 116 | thread_pool.Wait(); 117 | } 118 | 119 | std::cout << "\r Estimating relative pose: 100%" << std::endl; 120 | LOG(INFO) << "Estimating relative pose done"; 121 | } 122 | 123 | } // namespace glomap 124 | -------------------------------------------------------------------------------- /glomap/estimators/relpose_estimation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/types_sfm.h" 4 | 5 | #include 6 | 7 | namespace glomap { 8 | 9 | struct RelativePoseEstimationOptions { 10 | // Options for poselib solver 11 | poselib::RansacOptions ransac_options; 12 | poselib::BundleOptions bundle_options; 13 | 14 | RelativePoseEstimationOptions() { ransac_options.max_iterations = 50000; } 15 | }; 16 | 17 | void EstimateRelativePoses(ViewGraph& view_graph, 18 | std::unordered_map& cameras, 19 | std::unordered_map& images, 20 | const RelativePoseEstimationOptions& options); 21 | 22 | } // namespace glomap 23 | -------------------------------------------------------------------------------- /glomap/estimators/view_graph_calibration.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/estimators/view_graph_calibration.h" 2 | 3 | #include "glomap/estimators/cost_function.h" 4 | #include "glomap/math/two_view_geometry.h" 5 | 6 | #include 7 | 8 | #include 9 | 10 | namespace glomap { 11 | 12 | bool ViewGraphCalibrator::Solve(ViewGraph& view_graph, 13 | std::unordered_map& cameras, 14 | std::unordered_map& images) { 15 | // Reset the problem 16 | LOG(INFO) << "Start ViewGraphCalibrator"; 17 | 18 | Reset(cameras); 19 | 20 | // Set the solver options. 21 | if (cameras.size() < 50) 22 | options_.solver_options.linear_solver_type = ceres::DENSE_NORMAL_CHOLESKY; 23 | else 24 | options_.solver_options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY; 25 | 26 | // Add the image pairs into the problem 27 | AddImagePairsToProblem(view_graph, cameras, images); 28 | 29 | // Set the cameras to be constant if they have prior intrinsics 30 | const size_t num_cameras = ParameterizeCameras(cameras); 31 | 32 | if (num_cameras == 0) { 33 | LOG(INFO) << "No cameras to optimize"; 34 | return true; 35 | } 36 | 37 | // Solve the problem 38 | ceres::Solver::Summary summary; 39 | options_.solver_options.minimizer_progress_to_stdout = VLOG_IS_ON(2); 40 | ceres::Solve(options_.solver_options, problem_.get(), &summary); 41 | 42 | VLOG(2) << summary.FullReport(); 43 | 44 | // Convert the results back to the camera 45 | CopyBackResults(cameras); 46 | FilterImagePairs(view_graph); 47 | 48 | return summary.IsSolutionUsable(); 49 | } 50 | 51 | void ViewGraphCalibrator::Reset( 52 | const std::unordered_map& cameras) { 53 | // Initialize the problem 54 | focals_.clear(); 55 | focals_.reserve(cameras.size()); 56 | for (const auto& [camera_id, camera] : cameras) { 57 | focals_[camera_id] = camera.Focal(); 58 | } 59 | 60 | // Set up the problem 61 | ceres::Problem::Options problem_options; 62 | problem_options.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP; 63 | problem_ = std::make_unique(problem_options); 64 | loss_function_ = options_.CreateLossFunction(); 65 | } 66 | 67 | void ViewGraphCalibrator::AddImagePairsToProblem( 68 | const ViewGraph& view_graph, 69 | const std::unordered_map& cameras, 70 | const std::unordered_map& images) { 71 | for (auto& [image_pair_id, image_pair] : view_graph.image_pairs) { 72 | if (image_pair.config != colmap::TwoViewGeometry::CALIBRATED && 73 | image_pair.config != colmap::TwoViewGeometry::UNCALIBRATED) 74 | continue; 75 | if (image_pair.is_valid == false) continue; 76 | 77 | AddImagePair(image_pair, cameras, images); 78 | } 79 | } 80 | 81 | void ViewGraphCalibrator::AddImagePair( 82 | const ImagePair& image_pair, 83 | const std::unordered_map& cameras, 84 | const std::unordered_map& images) { 85 | const camera_t camera_id1 = images.at(image_pair.image_id1).camera_id; 86 | const camera_t camera_id2 = images.at(image_pair.image_id2).camera_id; 87 | 88 | if (camera_id1 == camera_id2) { 89 | problem_->AddResidualBlock( 90 | FetzerFocalLengthSameCameraCost::Create( 91 | image_pair.F, cameras.at(camera_id1).PrincipalPoint()), 92 | loss_function_.get(), 93 | &(focals_[camera_id1])); 94 | } else { 95 | problem_->AddResidualBlock( 96 | FetzerFocalLengthCost::Create(image_pair.F, 97 | cameras.at(camera_id1).PrincipalPoint(), 98 | cameras.at(camera_id2).PrincipalPoint()), 99 | loss_function_.get(), 100 | &(focals_[camera_id1]), 101 | &(focals_[camera_id2])); 102 | } 103 | } 104 | 105 | size_t ViewGraphCalibrator::ParameterizeCameras( 106 | const std::unordered_map& cameras) { 107 | size_t num_cameras = 0; 108 | for (auto& [camera_id, camera] : cameras) { 109 | if (!problem_->HasParameterBlock(&(focals_[camera_id]))) continue; 110 | 111 | num_cameras++; 112 | problem_->SetParameterLowerBound(&(focals_[camera_id]), 0, 1e-3); 113 | if (camera.has_prior_focal_length) { 114 | problem_->SetParameterBlockConstant(&(focals_[camera_id])); 115 | num_cameras--; 116 | } 117 | } 118 | 119 | return num_cameras; 120 | } 121 | 122 | void ViewGraphCalibrator::CopyBackResults( 123 | std::unordered_map& cameras) { 124 | size_t counter = 0; 125 | for (auto& [camera_id, camera] : cameras) { 126 | if (!problem_->HasParameterBlock(&(focals_[camera_id]))) continue; 127 | 128 | // if the estimated parameter is too crazy, reject it 129 | if (focals_[camera_id] / camera.Focal() > options_.thres_higher_ratio || 130 | focals_[camera_id] / camera.Focal() < options_.thres_lower_ratio) { 131 | VLOG(2) << "Ignoring degenerate camera camera " << camera_id 132 | << " focal: " << focals_[camera_id] 133 | << " original focal: " << camera.Focal(); 134 | counter++; 135 | 136 | continue; 137 | } 138 | 139 | // Marke that the camera has refined intrinsics 140 | camera.has_refined_focal_length = true; 141 | 142 | // Update the focal length 143 | for (const size_t idx : camera.FocalLengthIdxs()) { 144 | camera.params[idx] = focals_[camera_id]; 145 | } 146 | } 147 | LOG(INFO) << counter << " cameras are rejected in view graph calibration"; 148 | } 149 | 150 | size_t ViewGraphCalibrator::FilterImagePairs(ViewGraph& view_graph) const { 151 | ceres::Problem::EvaluateOptions eval_options; 152 | eval_options.num_threads = options_.solver_options.num_threads; 153 | eval_options.apply_loss_function = false; 154 | std::vector residuals; 155 | problem_->Evaluate(eval_options, nullptr, &residuals, nullptr, nullptr); 156 | 157 | // Dump the residuals into the original data structure 158 | size_t counter = 0; 159 | size_t invalid_counter = 0; 160 | 161 | const double thres_two_view_error_sq = 162 | options_.thres_two_view_error * options_.thres_two_view_error; 163 | 164 | for (auto& [image_pair_id, image_pair] : view_graph.image_pairs) { 165 | if (image_pair.config != colmap::TwoViewGeometry::CALIBRATED && 166 | image_pair.config != colmap::TwoViewGeometry::UNCALIBRATED) 167 | continue; 168 | if (image_pair.is_valid == false) continue; 169 | 170 | const Eigen::Vector2d error(residuals[counter], residuals[counter + 1]); 171 | 172 | // Set the two view geometry to be invalid if the error is too high 173 | if (error.squaredNorm() > thres_two_view_error_sq) { 174 | invalid_counter++; 175 | image_pair.is_valid = false; 176 | } 177 | 178 | counter += 2; 179 | } 180 | 181 | LOG(INFO) << "invalid / total number of two view geometry: " 182 | << invalid_counter << " / " << (counter / 2); 183 | 184 | return invalid_counter; 185 | } 186 | 187 | } // namespace glomap 188 | -------------------------------------------------------------------------------- /glomap/estimators/view_graph_calibration.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/estimators/optimization_base.h" 4 | #include "glomap/scene/types_sfm.h" 5 | 6 | #include 7 | 8 | namespace glomap { 9 | 10 | struct ViewGraphCalibratorOptions : public OptimizationBaseOptions { 11 | // The minimal ratio of the estimated focal length against the prior focal 12 | // length 13 | double thres_lower_ratio = 0.1; 14 | // The maximal ratio of the estimated focal length against the prior focal 15 | // length 16 | double thres_higher_ratio = 10; 17 | 18 | // The threshold for the corresponding error in the problem for an image pair 19 | double thres_two_view_error = 2.; 20 | 21 | ViewGraphCalibratorOptions() : OptimizationBaseOptions() { 22 | thres_loss_function = 1e-2; 23 | } 24 | 25 | std::shared_ptr CreateLossFunction() { 26 | return std::make_shared(thres_loss_function); 27 | } 28 | }; 29 | 30 | class ViewGraphCalibrator { 31 | public: 32 | ViewGraphCalibrator(const ViewGraphCalibratorOptions& options) 33 | : options_(options) {} 34 | 35 | // Entry point for the calibration 36 | bool Solve(ViewGraph& view_graph, 37 | std::unordered_map& cameras, 38 | std::unordered_map& images); 39 | 40 | private: 41 | // Reset the problem 42 | void Reset(const std::unordered_map& cameras); 43 | 44 | // Add the image pairs to the problem 45 | void AddImagePairsToProblem( 46 | const ViewGraph& view_graph, 47 | const std::unordered_map& cameras, 48 | const std::unordered_map& images); 49 | 50 | // Add a single image pair to the problem 51 | void AddImagePair(const ImagePair& image_pair, 52 | const std::unordered_map& cameras, 53 | const std::unordered_map& images); 54 | 55 | // Set the cameras to be constant if they have prior intrinsics 56 | size_t ParameterizeCameras( 57 | const std::unordered_map& cameras); 58 | 59 | // Convert the results back to the camera 60 | void CopyBackResults(std::unordered_map& cameras); 61 | 62 | // Filter the image pairs based on the calibration results 63 | size_t FilterImagePairs(ViewGraph& view_graph) const; 64 | 65 | ViewGraphCalibratorOptions options_; 66 | std::unique_ptr problem_; 67 | std::unordered_map focals_; 68 | std::shared_ptr loss_function_; 69 | }; 70 | 71 | } // namespace glomap 72 | -------------------------------------------------------------------------------- /glomap/exe/global_mapper.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/controllers/global_mapper.h" 2 | 3 | #include "glomap/controllers/option_manager.h" 4 | #include "glomap/io/colmap_io.h" 5 | #include "glomap/types.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | namespace glomap { 12 | // ------------------------------------- 13 | // Mappers starting from COLMAP database 14 | // ------------------------------------- 15 | int RunMapper(int argc, char** argv) { 16 | std::string database_path; 17 | std::string output_path; 18 | 19 | std::string image_path = ""; 20 | std::string constraint_type = "ONLY_POINTS"; 21 | std::string output_format = "bin"; 22 | 23 | OptionManager options; 24 | options.AddRequiredOption("database_path", &database_path); 25 | options.AddRequiredOption("output_path", &output_path); 26 | options.AddDefaultOption("image_path", &image_path); 27 | options.AddDefaultOption("constraint_type", 28 | &constraint_type, 29 | "{ONLY_POINTS, ONLY_CAMERAS, " 30 | "POINTS_AND_CAMERAS_BALANCED, POINTS_AND_CAMERAS}"); 31 | options.AddDefaultOption("output_format", &output_format, "{bin, txt}"); 32 | options.AddGlobalMapperFullOptions(); 33 | 34 | options.Parse(argc, argv); 35 | 36 | if (!colmap::ExistsFile(database_path)) { 37 | LOG(ERROR) << "`database_path` is not a file"; 38 | return EXIT_FAILURE; 39 | } 40 | 41 | if (constraint_type == "ONLY_POINTS") { 42 | options.mapper->opt_gp.constraint_type = 43 | GlobalPositionerOptions::ONLY_POINTS; 44 | } else if (constraint_type == "ONLY_CAMERAS") { 45 | options.mapper->opt_gp.constraint_type = 46 | GlobalPositionerOptions::ONLY_CAMERAS; 47 | } else if (constraint_type == "POINTS_AND_CAMERAS_BALANCED") { 48 | options.mapper->opt_gp.constraint_type = 49 | GlobalPositionerOptions::POINTS_AND_CAMERAS_BALANCED; 50 | } else if (constraint_type == "POINTS_AND_CAMERAS") { 51 | options.mapper->opt_gp.constraint_type = 52 | GlobalPositionerOptions::POINTS_AND_CAMERAS; 53 | } else { 54 | LOG(ERROR) << "Invalid constriant type"; 55 | return EXIT_FAILURE; 56 | } 57 | 58 | // Check whether output_format is valid 59 | if (output_format != "bin" && output_format != "txt") { 60 | LOG(ERROR) << "Invalid output format"; 61 | return EXIT_FAILURE; 62 | } 63 | 64 | // Load the database 65 | ViewGraph view_graph; 66 | std::unordered_map cameras; 67 | std::unordered_map images; 68 | std::unordered_map tracks; 69 | 70 | const colmap::Database database(database_path); 71 | ConvertDatabaseToGlomap(database, view_graph, cameras, images); 72 | 73 | if (view_graph.image_pairs.empty()) { 74 | LOG(ERROR) << "Can't continue without image pairs"; 75 | return EXIT_FAILURE; 76 | } 77 | 78 | GlobalMapper global_mapper(*options.mapper); 79 | 80 | // Main solver 81 | LOG(INFO) << "Loaded database"; 82 | colmap::Timer run_timer; 83 | run_timer.Start(); 84 | global_mapper.Solve(database, view_graph, cameras, images, tracks); 85 | run_timer.Pause(); 86 | 87 | LOG(INFO) << "Reconstruction done in " << run_timer.ElapsedSeconds() 88 | << " seconds"; 89 | 90 | WriteGlomapReconstruction( 91 | output_path, cameras, images, tracks, output_format, image_path); 92 | LOG(INFO) << "Export to COLMAP reconstruction done"; 93 | 94 | return EXIT_SUCCESS; 95 | } 96 | 97 | // ------------------------------------- 98 | // Mappers starting from COLMAP reconstruction 99 | // ------------------------------------- 100 | int RunMapperResume(int argc, char** argv) { 101 | std::string input_path; 102 | std::string output_path; 103 | std::string image_path = ""; 104 | std::string output_format = "bin"; 105 | 106 | OptionManager options; 107 | options.AddRequiredOption("input_path", &input_path); 108 | options.AddRequiredOption("output_path", &output_path); 109 | options.AddDefaultOption("image_path", &image_path); 110 | options.AddDefaultOption("output_format", &output_format, "{bin, txt}"); 111 | options.AddGlobalMapperResumeFullOptions(); 112 | 113 | options.Parse(argc, argv); 114 | 115 | if (!colmap::ExistsDir(input_path)) { 116 | LOG(ERROR) << "`input_path` is not a directory"; 117 | return EXIT_FAILURE; 118 | } 119 | 120 | // Check whether output_format is valid 121 | if (output_format != "bin" && output_format != "txt") { 122 | LOG(ERROR) << "Invalid output format"; 123 | return EXIT_FAILURE; 124 | } 125 | 126 | // Load the reconstruction 127 | ViewGraph view_graph; // dummy variable 128 | colmap::Database database; // dummy variable 129 | 130 | std::unordered_map cameras; 131 | std::unordered_map images; 132 | std::unordered_map tracks; 133 | colmap::Reconstruction reconstruction; 134 | reconstruction.Read(input_path); 135 | ConvertColmapToGlomap(reconstruction, cameras, images, tracks); 136 | 137 | GlobalMapper global_mapper(*options.mapper); 138 | 139 | // Main solver 140 | colmap::Timer run_timer; 141 | run_timer.Start(); 142 | global_mapper.Solve(database, view_graph, cameras, images, tracks); 143 | run_timer.Pause(); 144 | 145 | LOG(INFO) << "Reconstruction done in " << run_timer.ElapsedSeconds() 146 | << " seconds"; 147 | 148 | WriteGlomapReconstruction( 149 | output_path, cameras, images, tracks, output_format, image_path); 150 | LOG(INFO) << "Export to COLMAP reconstruction done"; 151 | 152 | return EXIT_SUCCESS; 153 | } 154 | 155 | } // namespace glomap 156 | -------------------------------------------------------------------------------- /glomap/exe/global_mapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/controllers/global_mapper.h" 4 | 5 | namespace glomap { 6 | 7 | // Use default values for most of the settings from database 8 | int RunMapper(int argc, char** argv); 9 | 10 | // Use default values for most of the settings from colmap reconstruction 11 | int RunMapperResume(int argc, char** argv); 12 | 13 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/exe/rotation_averager.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "glomap/controllers/rotation_averager.h" 3 | 4 | #include "glomap/controllers/option_manager.h" 5 | #include "glomap/estimators/gravity_refinement.h" 6 | #include "glomap/io/colmap_io.h" 7 | #include "glomap/io/pose_io.h" 8 | #include "glomap/types.h" 9 | 10 | #include 11 | #include 12 | 13 | namespace glomap { 14 | // ------------------------------------- 15 | // Running Global Rotation Averager 16 | // ------------------------------------- 17 | int RunRotationAverager(int argc, char** argv) { 18 | std::string relpose_path; 19 | std::string output_path; 20 | std::string gravity_path = ""; 21 | std::string weight_path = ""; 22 | 23 | bool use_stratified = true; 24 | bool refine_gravity = false; 25 | bool use_weight = false; 26 | 27 | OptionManager options; 28 | options.AddRequiredOption("relpose_path", &relpose_path); 29 | options.AddRequiredOption("output_path", &output_path); 30 | options.AddDefaultOption("gravity_path", &gravity_path); 31 | options.AddDefaultOption("weight_path", &weight_path); 32 | options.AddDefaultOption("use_stratified", &use_stratified); 33 | options.AddDefaultOption("refine_gravity", &refine_gravity); 34 | options.AddDefaultOption("use_weight", &use_weight); 35 | options.AddGravityRefinerOptions(); 36 | options.Parse(argc, argv); 37 | 38 | if (!colmap::ExistsFile(relpose_path)) { 39 | LOG(ERROR) << "`relpose_path` is not a file"; 40 | return EXIT_FAILURE; 41 | } 42 | 43 | if (gravity_path != "" && !colmap::ExistsFile(gravity_path)) { 44 | LOG(ERROR) << "`gravity_path` is not a file"; 45 | return EXIT_FAILURE; 46 | } 47 | 48 | if (weight_path != "" && !colmap::ExistsFile(weight_path)) { 49 | LOG(ERROR) << "`weight_path` is not a file"; 50 | return EXIT_FAILURE; 51 | } 52 | 53 | if (use_weight && weight_path == "") { 54 | LOG(ERROR) << "Weight path is required when use_weight is set to true"; 55 | return EXIT_FAILURE; 56 | } 57 | 58 | RotationAveragerOptions rotation_averager_options; 59 | rotation_averager_options.skip_initialization = true; 60 | rotation_averager_options.use_gravity = true; 61 | 62 | rotation_averager_options.use_stratified = use_stratified; 63 | rotation_averager_options.use_weight = use_weight; 64 | 65 | // Load the database 66 | ViewGraph view_graph; 67 | std::unordered_map images; 68 | 69 | ReadRelPose(relpose_path, images, view_graph); 70 | 71 | if (gravity_path != "") { 72 | ReadGravity(gravity_path, images); 73 | } 74 | 75 | if (use_weight) { 76 | ReadRelWeight(weight_path, images, view_graph); 77 | } 78 | 79 | int num_img = view_graph.KeepLargestConnectedComponents(images); 80 | LOG(INFO) << num_img << " / " << images.size() 81 | << " are within the largest connected component"; 82 | 83 | if (refine_gravity && gravity_path != "") { 84 | GravityRefiner grav_refiner(*options.gravity_refiner); 85 | grav_refiner.RefineGravity(view_graph, images); 86 | } 87 | 88 | colmap::Timer run_timer; 89 | run_timer.Start(); 90 | if (!SolveRotationAveraging(view_graph, images, rotation_averager_options)) { 91 | LOG(ERROR) << "Failed to solve global rotation averaging"; 92 | return EXIT_FAILURE; 93 | } 94 | run_timer.Pause(); 95 | LOG(INFO) << "Global rotation averaging done in " 96 | << run_timer.ElapsedSeconds() << " seconds"; 97 | 98 | // Write out the estimated rotation 99 | WriteGlobalRotation(output_path, images); 100 | LOG(INFO) << "Global rotation averaging done" << std::endl; 101 | 102 | return EXIT_SUCCESS; 103 | } 104 | 105 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/exe/rotation_averager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/estimators/global_rotation_averaging.h" 4 | 5 | namespace glomap { 6 | 7 | // Use default values for most of the settings from database 8 | int RunRotationAverager(int argc, char** argv); 9 | 10 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/glomap.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/exe/global_mapper.h" 2 | #include "glomap/exe/rotation_averager.h" 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace { 9 | 10 | typedef std::function command_func_t; 11 | int ShowHelp( 12 | const std::vector>& commands) { 13 | std::cout << "GLOMAP -- Global Structure-from-Motion" << std::endl 14 | << std::endl; 15 | 16 | #ifdef GLOMAP_CUDA_ENABLED 17 | std::cout << "This version was compiled with CUDA!" << std::endl << std::endl; 18 | #else 19 | std::cout << "This version was NOT compiled CUDA!" << std::endl << std::endl; 20 | #endif 21 | 22 | std::cout << "Usage:" << std::endl; 23 | std::cout << " glomap mapper --database_path DATABASE --output_path MODEL" 24 | << std::endl; 25 | std::cout << " glomap mapper_resume --input_path MODEL_INPUT --output_path " 26 | "MODEL_OUTPUT" 27 | << std::endl; 28 | 29 | std::cout << "Available commands:" << std::endl; 30 | std::cout << " help" << std::endl; 31 | for (const auto& command : commands) { 32 | std::cout << " " << command.first << std::endl; 33 | } 34 | std::cout << std::endl; 35 | 36 | return EXIT_SUCCESS; 37 | } 38 | 39 | } // namespace 40 | 41 | int main(int argc, char** argv) { 42 | colmap::InitializeGlog(argv); 43 | FLAGS_alsologtostderr = true; 44 | 45 | std::vector> commands; 46 | commands.emplace_back("mapper", &glomap::RunMapper); 47 | commands.emplace_back("mapper_resume", &glomap::RunMapperResume); 48 | commands.emplace_back("rotation_averager", &glomap::RunRotationAverager); 49 | 50 | if (argc == 1) { 51 | return ShowHelp(commands); 52 | } 53 | 54 | const std::string command = argv[1]; 55 | if (command == "help" || command == "-h" || command == "--help") { 56 | return ShowHelp(commands); 57 | } else { 58 | command_func_t matched_command_func = nullptr; 59 | for (const auto& command_func : commands) { 60 | if (command == command_func.first) { 61 | matched_command_func = command_func.second; 62 | break; 63 | } 64 | } 65 | if (matched_command_func == nullptr) { 66 | std::cout << "Command " << command << " not recognized. " 67 | << "To list the available commands, run `glomap help`." 68 | << std::endl; 69 | return EXIT_FAILURE; 70 | } else { 71 | int command_argc = argc - 1; 72 | char** command_argv = &argv[1]; 73 | command_argv[0] = argv[0]; 74 | return matched_command_func(command_argc, command_argv); 75 | } 76 | } 77 | 78 | return ShowHelp(commands); 79 | } 80 | -------------------------------------------------------------------------------- /glomap/io/colmap_converter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "glomap/scene/types_sfm.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace glomap { 9 | 10 | void ConvertGlomapToColmapImage(const Image& image, 11 | colmap::Image& colmap_image, 12 | bool keep_points = false); 13 | 14 | void ConvertGlomapToColmap(const std::unordered_map& cameras, 15 | const std::unordered_map& images, 16 | const std::unordered_map& tracks, 17 | colmap::Reconstruction& reconstruction, 18 | int cluster_id = -1, 19 | bool include_image_points = false); 20 | 21 | void ConvertColmapToGlomap(const colmap::Reconstruction& reconstruction, 22 | std::unordered_map& cameras, 23 | std::unordered_map& images, 24 | std::unordered_map& tracks); 25 | 26 | void ConvertColmapPoints3DToGlomapTracks( 27 | const colmap::Reconstruction& reconstruction, 28 | std::unordered_map& tracks); 29 | 30 | void ConvertDatabaseToGlomap(const colmap::Database& database, 31 | ViewGraph& view_graph, 32 | std::unordered_map& cameras, 33 | std::unordered_map& images); 34 | 35 | } // namespace glomap 36 | -------------------------------------------------------------------------------- /glomap/io/colmap_io.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/io/colmap_io.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace glomap { 7 | 8 | void WriteGlomapReconstruction( 9 | const std::string& reconstruction_path, 10 | const std::unordered_map& cameras, 11 | const std::unordered_map& images, 12 | const std::unordered_map& tracks, 13 | const std::string output_format, 14 | const std::string image_path) { 15 | // Check whether reconstruction pruning is applied. 16 | // If so, export seperate reconstruction 17 | int largest_component_num = -1; 18 | for (const auto& [image_id, image] : images) { 19 | if (image.cluster_id > largest_component_num) 20 | largest_component_num = image.cluster_id; 21 | } 22 | // If it is not seperated into several clusters, then output them as whole 23 | if (largest_component_num == -1) { 24 | colmap::Reconstruction reconstruction; 25 | ConvertGlomapToColmap(cameras, images, tracks, reconstruction); 26 | // Read in colors 27 | if (image_path != "") { 28 | LOG(INFO) << "Extracting colors ..."; 29 | reconstruction.ExtractColorsForAllImages(image_path); 30 | } 31 | colmap::CreateDirIfNotExists(reconstruction_path + "/0", true); 32 | if (output_format == "txt") { 33 | reconstruction.WriteText(reconstruction_path + "/0"); 34 | } else if (output_format == "bin") { 35 | reconstruction.WriteBinary(reconstruction_path + "/0"); 36 | } else { 37 | LOG(ERROR) << "Unsupported output type"; 38 | } 39 | } else { 40 | for (int comp = 0; comp <= largest_component_num; comp++) { 41 | std::cout << "\r Exporting reconstruction " << comp + 1 << " / " 42 | << largest_component_num + 1 << std::flush; 43 | colmap::Reconstruction reconstruction; 44 | ConvertGlomapToColmap(cameras, images, tracks, reconstruction, comp); 45 | // Read in colors 46 | if (image_path != "") { 47 | reconstruction.ExtractColorsForAllImages(image_path); 48 | } 49 | colmap::CreateDirIfNotExists( 50 | reconstruction_path + "/" + std::to_string(comp), true); 51 | if (output_format == "txt") { 52 | reconstruction.WriteText(reconstruction_path + "/" + 53 | std::to_string(comp)); 54 | } else if (output_format == "bin") { 55 | reconstruction.WriteBinary(reconstruction_path + "/" + 56 | std::to_string(comp)); 57 | } else { 58 | LOG(ERROR) << "Unsupported output type"; 59 | } 60 | } 61 | std::cout << std::endl; 62 | } 63 | } 64 | 65 | void WriteColmapReconstruction(const std::string& reconstruction_path, 66 | const colmap::Reconstruction& reconstruction, 67 | const std::string output_format) { 68 | colmap::CreateDirIfNotExists(reconstruction_path, true); 69 | if (output_format == "txt") { 70 | reconstruction.WriteText(reconstruction_path); 71 | } else if (output_format == "bin") { 72 | reconstruction.WriteBinary(reconstruction_path); 73 | } else { 74 | LOG(ERROR) << "Unsupported output type"; 75 | } 76 | } 77 | 78 | } // namespace glomap 79 | -------------------------------------------------------------------------------- /glomap/io/colmap_io.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/io/colmap_converter.h" 4 | #include "glomap/scene/types_sfm.h" 5 | 6 | namespace glomap { 7 | 8 | void WriteGlomapReconstruction( 9 | const std::string& reconstruction_path, 10 | const std::unordered_map& cameras, 11 | const std::unordered_map& images, 12 | const std::unordered_map& tracks, 13 | const std::string output_format = "bin", 14 | const std::string image_path = ""); 15 | 16 | void WriteColmapReconstruction(const std::string& reconstruction_path, 17 | const colmap::Reconstruction& reconstruction, 18 | const std::string output_format = "bin"); 19 | 20 | } // namespace glomap 21 | -------------------------------------------------------------------------------- /glomap/io/pose_io.cc: -------------------------------------------------------------------------------- 1 | #include "pose_io.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace glomap { 8 | void ReadRelPose(const std::string& file_path, 9 | std::unordered_map& images, 10 | ViewGraph& view_graph) { 11 | std::unordered_map name_idx; 12 | image_t max_image_id = 0; 13 | for (const auto& [image_id, image] : images) { 14 | name_idx[image.file_name] = image_id; 15 | 16 | max_image_id = std::max(max_image_id, image_id); 17 | } 18 | 19 | std::ifstream file(file_path); 20 | 21 | // Read in data 22 | std::string line; 23 | std::string item; 24 | 25 | size_t counter = 0; 26 | 27 | // Required data structures 28 | // IMAGE_NAME_1 IMAGE_NAME_2 QW QX QY QZ TX TY TZ 29 | while (std::getline(file, line)) { 30 | std::stringstream line_stream(line); 31 | 32 | std::string file1, file2; 33 | std::getline(line_stream, item, ' '); 34 | file1 = item; 35 | std::getline(line_stream, item, ' '); 36 | file2 = item; 37 | 38 | if (name_idx.find(file1) == name_idx.end()) { 39 | max_image_id += 1; 40 | images.insert( 41 | std::make_pair(max_image_id, Image(max_image_id, -1, file1))); 42 | name_idx[file1] = max_image_id; 43 | } 44 | if (name_idx.find(file2) == name_idx.end()) { 45 | max_image_id += 1; 46 | images.insert( 47 | std::make_pair(max_image_id, Image(max_image_id, -1, file2))); 48 | name_idx[file2] = max_image_id; 49 | } 50 | 51 | image_t index1 = name_idx[file1]; 52 | image_t index2 = name_idx[file2]; 53 | 54 | image_pair_t pair_id = ImagePair::ImagePairToPairId(index1, index2); 55 | 56 | // rotation 57 | Rigid3d pose_rel; 58 | for (int i = 0; i < 4; i++) { 59 | std::getline(line_stream, item, ' '); 60 | pose_rel.rotation.coeffs()[(i + 3) % 4] = std::stod(item); 61 | } 62 | 63 | for (int i = 0; i < 3; i++) { 64 | std::getline(line_stream, item, ' '); 65 | pose_rel.translation[i] = std::stod(item); 66 | } 67 | 68 | view_graph.image_pairs.insert( 69 | std::make_pair(pair_id, ImagePair(index1, index2, pose_rel))); 70 | counter++; 71 | } 72 | LOG(INFO) << counter << " relpose are loaded" << std::endl; 73 | } 74 | 75 | void ReadRelWeight(const std::string& file_path, 76 | const std::unordered_map& images, 77 | ViewGraph& view_graph) { 78 | std::unordered_map name_idx; 79 | for (const auto& [image_id, image] : images) { 80 | name_idx[image.file_name] = image_id; 81 | } 82 | 83 | std::ifstream file(file_path); 84 | 85 | // Read in data 86 | std::string line; 87 | std::string item; 88 | 89 | size_t counter = 0; 90 | 91 | // Required data structures 92 | // IMAGE_NAME_1 IMAGE_NAME_2 QW QX QY QZ TX TY TZ 93 | while (std::getline(file, line)) { 94 | std::stringstream line_stream(line); 95 | 96 | std::string file1, file2; 97 | std::getline(line_stream, item, ' '); 98 | file1 = item; 99 | std::getline(line_stream, item, ' '); 100 | file2 = item; 101 | 102 | if (name_idx.find(file1) == name_idx.end() || 103 | name_idx.find(file2) == name_idx.end()) 104 | continue; 105 | 106 | image_t index1 = name_idx[file1]; 107 | image_t index2 = name_idx[file2]; 108 | 109 | image_pair_t pair_id = ImagePair::ImagePairToPairId(index1, index2); 110 | 111 | if (view_graph.image_pairs.find(pair_id) == view_graph.image_pairs.end()) 112 | continue; 113 | 114 | std::getline(line_stream, item, ' '); 115 | view_graph.image_pairs[pair_id].weight = std::stod(item); 116 | counter++; 117 | } 118 | LOG(INFO) << counter << " weights are used are loaded" << std::endl; 119 | } 120 | 121 | void ReadGravity(const std::string& gravity_path, 122 | std::unordered_map& images) { 123 | std::unordered_map name_idx; 124 | for (const auto& [image_id, image] : images) { 125 | name_idx[image.file_name] = image_id; 126 | } 127 | 128 | std::ifstream file(gravity_path); 129 | 130 | // Read in the file list 131 | std::string line, item; 132 | Eigen::Vector3d gravity; 133 | int counter = 0; 134 | while (std::getline(file, line)) { 135 | std::stringstream line_stream(line); 136 | 137 | // file_name 138 | std::string name; 139 | std::getline(line_stream, name, ' '); 140 | 141 | // Gravity 142 | for (double i = 0; i < 3; i++) { 143 | std::getline(line_stream, item, ' '); 144 | gravity[i] = std::stod(item); 145 | } 146 | 147 | // Check whether the image present 148 | auto ite = name_idx.find(name); 149 | if (ite != name_idx.end()) { 150 | counter++; 151 | images[ite->second].gravity_info.SetGravity(gravity); 152 | // Make sure the initialization is aligned with the gravity 153 | images[ite->second].cam_from_world.rotation = 154 | images[ite->second].gravity_info.GetRAlign().transpose(); 155 | } 156 | } 157 | LOG(INFO) << counter << " images are loaded with gravity" << std::endl; 158 | } 159 | 160 | void WriteGlobalRotation(const std::string& file_path, 161 | const std::unordered_map& images) { 162 | std::ofstream file(file_path); 163 | std::set existing_images; 164 | for (const auto& [image_id, image] : images) { 165 | if (image.is_registered) { 166 | existing_images.insert(image_id); 167 | } 168 | } 169 | for (const auto& image_id : existing_images) { 170 | const auto image = images.at(image_id); 171 | if (!image.is_registered) continue; 172 | file << image.file_name; 173 | for (int i = 0; i < 4; i++) { 174 | file << " " << image.cam_from_world.rotation.coeffs()[(i + 3) % 4]; 175 | } 176 | file << "\n"; 177 | } 178 | } 179 | 180 | void WriteRelPose(const std::string& file_path, 181 | const std::unordered_map& images, 182 | const ViewGraph& view_graph) { 183 | std::ofstream file(file_path); 184 | 185 | // Sort the image pairs by image name 186 | std::map name_pair; 187 | for (const auto& [pair_id, image_pair] : view_graph.image_pairs) { 188 | if (image_pair.is_valid) { 189 | const auto image1 = images.at(image_pair.image_id1); 190 | const auto image2 = images.at(image_pair.image_id2); 191 | name_pair[image1.file_name + " " + image2.file_name] = pair_id; 192 | } 193 | } 194 | 195 | // Write the image pairs 196 | for (const auto& [name, pair_id] : name_pair) { 197 | const auto image_pair = view_graph.image_pairs.at(pair_id); 198 | if (!image_pair.is_valid) continue; 199 | file << images.at(image_pair.image_id1).file_name << " " 200 | << images.at(image_pair.image_id2).file_name; 201 | for (int i = 0; i < 4; i++) { 202 | file << " " << image_pair.cam2_from_cam1.rotation.coeffs()[(i + 3) % 4]; 203 | } 204 | for (int i = 0; i < 3; i++) { 205 | file << " " << image_pair.cam2_from_cam1.translation[i]; 206 | } 207 | file << "\n"; 208 | } 209 | 210 | LOG(INFO) << name_pair.size() << " relpose are written" << std::endl; 211 | } 212 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/io/pose_io.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/types_sfm.h" 4 | 5 | #include 6 | 7 | namespace glomap { 8 | // Required data structures 9 | // IMAGE_NAME_1 IMAGE_NAME_2 QW QX QY QZ TX TY TZ 10 | void ReadRelPose(const std::string& file_path, 11 | std::unordered_map& images, 12 | ViewGraph& view_graph); 13 | 14 | // Required data structures 15 | // IMAGE_NAME_1 IMAGE_NAME_2 weight 16 | void ReadRelWeight(const std::string& file_path, 17 | const std::unordered_map& images, 18 | ViewGraph& view_graph); 19 | 20 | // Require the gravity in the format: 21 | // IMAGE_NAME GX GY GZ 22 | // Gravity should be the direction of [0,1,0] in the image frame 23 | // image.cam_from_world * [0,1,0]^T = g 24 | void ReadGravity(const std::string& gravity_path, 25 | std::unordered_map& images); 26 | 27 | // Output would be of the format: 28 | // IMAGE_NAME QW QX QY QZ 29 | void WriteGlobalRotation(const std::string& file_path, 30 | const std::unordered_map& images); 31 | 32 | // Output would be of the format: 33 | // IMAGE_NAME_1 IMAGE_NAME_2 QW QX QY QZ TX TY TZ 34 | void WriteRelPose(const std::string& file_path, 35 | const std::unordered_map& images, 36 | const ViewGraph& view_graph); 37 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/math/gravity.cc: -------------------------------------------------------------------------------- 1 | #include "gravity.h" 2 | 3 | #include "glomap/math/rigid3d.h" 4 | #include "glomap/scene/types_sfm.h" 5 | 6 | #include 7 | 8 | namespace glomap { 9 | 10 | // The second col of R_align is gravity direction 11 | Eigen::Matrix3d GetAlignRot(const Eigen::Vector3d& gravity) { 12 | Eigen::Matrix3d R; 13 | Eigen::Vector3d v = gravity.normalized(); 14 | R.col(1) = v; 15 | 16 | Eigen::Matrix3d Q = v.householderQr().householderQ(); 17 | Eigen::Matrix N = Q.rightCols(2); 18 | R.col(0) = N.col(0); 19 | R.col(2) = N.col(1); 20 | if (R.determinant() < 0) { 21 | R.col(2) = -R.col(2); 22 | } 23 | return R; 24 | } 25 | 26 | double RotUpToAngle(const Eigen::Matrix3d& R_up) { 27 | return RotationToAngleAxis(R_up)[1]; 28 | } 29 | 30 | Eigen::Matrix3d AngleToRotUp(double angle) { 31 | Eigen::Vector3d aa(0, angle, 0); 32 | return AngleAxisToRotation(aa); 33 | } 34 | 35 | // Code adapted from 36 | // https://gist.github.com/PeteBlackerThe3rd/f73e9d569e29f23e8bd828d7886636a0 37 | Eigen::Vector3d AverageGravity(const std::vector& gravities) { 38 | if (gravities.size() == 0) { 39 | std::cerr 40 | << "Error trying to calculate the average gravities of an empty set!\n"; 41 | return Eigen::Vector3d::Zero(); 42 | } 43 | 44 | // first build a 3x3 matrix which is the elementwise sum of the product of 45 | // each quaternion with itself 46 | Eigen::Matrix3d A = Eigen::Matrix3d::Zero(); 47 | 48 | for (int g = 0; g < gravities.size(); ++g) 49 | A += gravities[g] * gravities[g].transpose(); 50 | 51 | // normalise with the number of gravities 52 | A /= gravities.size(); 53 | 54 | // Compute the SVD of this 3x3 matrix 55 | Eigen::JacobiSVD svd( 56 | A, Eigen::ComputeThinU | Eigen::ComputeThinV); 57 | 58 | Eigen::VectorXd singular_values = svd.singularValues(); 59 | Eigen::MatrixXd U = svd.matrixU(); 60 | 61 | // find the eigen vector corresponding to the largest eigen value 62 | int largest_eigen_value_index = -1; 63 | float largest_eigen_value; 64 | bool first = true; 65 | 66 | for (int i = 0; i < singular_values.rows(); ++i) { 67 | if (first) { 68 | largest_eigen_value = singular_values(i); 69 | largest_eigen_value_index = i; 70 | first = false; 71 | } else if (singular_values(i) > largest_eigen_value) { 72 | largest_eigen_value = singular_values(i); 73 | largest_eigen_value_index = i; 74 | } 75 | } 76 | 77 | Eigen::Vector3d average; 78 | average(0) = U(0, largest_eigen_value_index); 79 | average(1) = U(1, largest_eigen_value_index); 80 | average(2) = U(2, largest_eigen_value_index); 81 | 82 | int negative_counter = 0; 83 | for (int g = 0; g < gravities.size(); ++g) { 84 | if (gravities[g].dot(average) < 0) negative_counter++; 85 | } 86 | if (negative_counter > gravities.size() / 2) { 87 | average = -average; 88 | } 89 | 90 | return average; 91 | } 92 | 93 | double CalcAngle(const Eigen::Vector3d& gravity1, 94 | const Eigen::Vector3d& gravity2) { 95 | double cos_r = gravity1.dot(gravity2) / (gravity1.norm() * gravity2.norm()); 96 | cos_r = std::min(std::max(cos_r, -1.), 1.); 97 | 98 | return std::acos(cos_r) * 180 / EIGEN_PI; 99 | } 100 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/math/gravity.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace glomap { 6 | 7 | // Get the aligment rotation matrix by QR decomposition 8 | // The second col of output is gravity direction 9 | Eigen::Matrix3d GetAlignRot(const Eigen::Vector3d& gravity); 10 | 11 | // Get the rotation angle for an upright rotation matrix 12 | double RotUpToAngle(const Eigen::Matrix3d& R_up); 13 | 14 | // Get the upright rotation matrix from a rotation angle 15 | Eigen::Matrix3d AngleToRotUp(double angle); 16 | 17 | // Estimate the average gravity direction from a set of gravity directions 18 | Eigen::Vector3d AverageGravity(const std::vector& gravities); 19 | 20 | double CalcAngle(const Eigen::Vector3d& gravity1, 21 | const Eigen::Vector3d& gravity2); 22 | } // namespace glomap 23 | -------------------------------------------------------------------------------- /glomap/math/l1_solver.h: -------------------------------------------------------------------------------- 1 | // This code is adapted from Theia library (http://theia-sfm.org/), 2 | // with its original L1 solver adapted from 3 | // "https://web.stanford.edu/~boyd/papers/admm/least_abs_deviations/lad.html" 4 | 5 | #pragma once 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | // An L1 norm (|| A * x - b ||_1) approximation solver based on ADMM 14 | // (alternating direction method of multipliers, 15 | // https://web.stanford.edu/~boyd/papers/pdf/admm_distr_stats.pdf). 16 | namespace glomap { 17 | 18 | // TODO: L1 solver for dense matrix 19 | struct L1SolverOptions { 20 | int max_num_iterations = 1000; 21 | // Rho is the augmented Lagrangian parameter. 22 | double rho = 1.0; 23 | // Alpha is the over-relaxation parameter (typically between 1.0 and 1.8). 24 | double alpha = 1.0; 25 | 26 | double absolute_tolerance = 1e-4; 27 | double relative_tolerance = 1e-2; 28 | }; 29 | 30 | template 31 | class L1Solver { 32 | public: 33 | L1Solver(const L1SolverOptions& options, const MatrixType& mat) 34 | : options_(options), a_(mat) { 35 | // Pre-compute the sparsity pattern. 36 | const MatrixType spd_mat = a_.transpose() * a_; 37 | linear_solver_.compute(spd_mat); 38 | } 39 | 40 | void Solve(const Eigen::VectorXd& rhs, Eigen::VectorXd* solution) { 41 | Eigen::VectorXd& x = *solution; 42 | Eigen::VectorXd z(a_.rows()), u(a_.rows()); 43 | z.setZero(); 44 | u.setZero(); 45 | 46 | Eigen::VectorXd a_times_x(a_.rows()), z_old(z.size()), ax_hat(a_.rows()); 47 | // Precompute some convergence terms. 48 | const double rhs_norm = rhs.norm(); 49 | const double primal_abs_tolerance_eps = 50 | std::sqrt(a_.rows()) * options_.absolute_tolerance; 51 | const double dual_abs_tolerance_eps = 52 | std::sqrt(a_.cols()) * options_.absolute_tolerance; 53 | 54 | const std::string row_format = 55 | " % 4d % 4.4e % 4.4e % 4.4e % 4.4e"; 56 | for (int i = 0; i < options_.max_num_iterations; i++) { 57 | // Update x. 58 | x.noalias() = linear_solver_.solve(a_.transpose() * (rhs + z - u)); 59 | if (linear_solver_.info() != Eigen::Success) { 60 | LOG(ERROR) << "L1 Minimization failed. Could not solve the sparse " 61 | "linear system with Cholesky Decomposition"; 62 | return; 63 | } 64 | 65 | a_times_x.noalias() = a_ * x; 66 | ax_hat.noalias() = options_.alpha * a_times_x; 67 | ax_hat.noalias() += (1.0 - options_.alpha) * (z + rhs); 68 | 69 | // Update z and set z_old. 70 | std::swap(z, z_old); 71 | z.noalias() = Shrinkage(ax_hat - rhs + u, 1.0 / options_.rho); 72 | 73 | // Update u. 74 | u.noalias() += ax_hat - z - rhs; 75 | 76 | // Compute the convergence terms. 77 | const double r_norm = (a_times_x - z - rhs).norm(); 78 | const double s_norm = 79 | (-options_.rho * a_.transpose() * (z - z_old)).norm(); 80 | const double max_norm = std::max({a_times_x.norm(), z.norm(), rhs_norm}); 81 | const double primal_eps = 82 | primal_abs_tolerance_eps + options_.relative_tolerance * max_norm; 83 | const double dual_eps = dual_abs_tolerance_eps + 84 | options_.relative_tolerance * 85 | (options_.rho * a_.transpose() * u).norm(); 86 | 87 | // Determine if the minimizer has converged. 88 | if (r_norm < primal_eps && s_norm < dual_eps) { 89 | break; 90 | } 91 | } 92 | } 93 | 94 | private: 95 | const L1SolverOptions& options_; 96 | 97 | // Matrix A in || Ax - b ||_1 98 | const MatrixType a_; 99 | 100 | // Cholesky linear solver. Since our linear system will be a SPD matrix we can 101 | // utilize the Cholesky factorization. 102 | Eigen::CholmodSupernodalLLT> linear_solver_; 103 | 104 | static Eigen::VectorXd Shrinkage(const Eigen::VectorXd& vec, 105 | const double kappa) { 106 | Eigen::ArrayXd zero_vec(vec.size()); 107 | zero_vec.setZero(); 108 | return zero_vec.max(vec.array() - kappa) - 109 | zero_vec.max(-vec.array() - kappa); 110 | } 111 | }; 112 | 113 | } // namespace glomap 114 | -------------------------------------------------------------------------------- /glomap/math/rigid3d.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/math/rigid3d.h" 2 | 3 | #include "glomap/scene/camera.h" 4 | 5 | namespace glomap { 6 | 7 | double CalcAngle(const Rigid3d& pose1, const Rigid3d& pose2) { 8 | double cos_r = 9 | ((pose1.rotation.inverse() * pose2.rotation).toRotationMatrix().trace() - 10 | 1) / 11 | 2; 12 | cos_r = std::min(std::max(cos_r, -1.), 1.); 13 | 14 | return std::acos(cos_r) * 180 / EIGEN_PI; 15 | } 16 | 17 | double CalcTrans(const Rigid3d& pose1, const Rigid3d& pose2) { 18 | return (Inverse(pose1).translation - Inverse(pose2).translation).norm(); 19 | } 20 | 21 | double CalcTransAngle(const Rigid3d& pose1, const Rigid3d& pose2) { 22 | double cos_r = (pose1.translation).dot(pose2.translation) / 23 | (pose1.translation.norm() * pose2.translation.norm()); 24 | cos_r = std::min(std::max(cos_r, -1.), 1.); 25 | 26 | return std::acos(cos_r) * 180 / EIGEN_PI; 27 | } 28 | 29 | double CalcAngle(const Eigen::Matrix3d& rotation1, 30 | const Eigen::Matrix3d& rotation2) { 31 | double cos_r = ((rotation1.transpose() * rotation2).trace() - 1) / 2; 32 | cos_r = std::min(std::max(cos_r, -1.), 1.); 33 | 34 | return std::acos(cos_r) * 180 / EIGEN_PI; 35 | } 36 | 37 | double DegToRad(double degree) { return degree * EIGEN_PI / 180; } 38 | 39 | double RadToDeg(double radian) { return radian * 180 / EIGEN_PI; } 40 | 41 | Eigen::Vector3d Rigid3dToAngleAxis(const Rigid3d& pose) { 42 | Eigen::AngleAxis aa(pose.rotation); 43 | Eigen::Vector3d aa_vec = aa.angle() * aa.axis(); 44 | return aa_vec; 45 | } 46 | 47 | Eigen::Vector3d RotationToAngleAxis(const Eigen::Matrix3d& rot) { 48 | Eigen::AngleAxis aa(rot); 49 | Eigen::Vector3d aa_vec = aa.angle() * aa.axis(); 50 | return aa_vec; 51 | } 52 | 53 | Eigen::Matrix3d AngleAxisToRotation(const Eigen::Vector3d& aa_vec) { 54 | double aa_norm = aa_vec.norm(); 55 | if (aa_norm > EPS) { 56 | return Eigen::AngleAxis(aa_norm, aa_vec.normalized()) 57 | .toRotationMatrix(); 58 | } else { 59 | Eigen::Matrix3d R; 60 | R(0, 0) = 1; 61 | R(1, 0) = aa_vec[2]; 62 | R(2, 0) = -aa_vec[1]; 63 | R(0, 1) = -aa_vec[2]; 64 | R(1, 1) = 1; 65 | R(2, 1) = aa_vec[0]; 66 | R(0, 2) = aa_vec[1]; 67 | R(1, 2) = -aa_vec[0]; 68 | R(2, 2) = 1; 69 | return R; 70 | } 71 | } 72 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/math/rigid3d.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/types.h" 4 | #include "glomap/types.h" 5 | 6 | #include 7 | 8 | namespace glomap { 9 | 10 | // Calculate the rotation angle difference between two poses 11 | double CalcAngle(const Rigid3d& pose1, const Rigid3d& pose2); 12 | 13 | // Calculate the center difference between two poses 14 | double CalcTrans(const Rigid3d& pose1, const Rigid3d& pose2); 15 | 16 | // Calculatet the translation direction difference between two poses 17 | double CalcTransAngle(const Rigid3d& pose1, const Rigid3d& pose2); 18 | 19 | // Calculate the rotation angle difference between two rotations 20 | double CalcAngle(const Eigen::Matrix3d& rotation1, 21 | const Eigen::Matrix3d& rotation2); 22 | 23 | // Convert degree to radian 24 | double DegToRad(double degree); 25 | 26 | // Convert radian to degree 27 | double RadToDeg(double radian); 28 | 29 | // Convert pose to angle axis 30 | Eigen::Vector3d Rigid3dToAngleAxis(const Rigid3d& pose); 31 | 32 | // Convert rotation matrix to angle axis 33 | Eigen::Vector3d RotationToAngleAxis(const Eigen::Matrix3d& rot); 34 | 35 | // Convert angle axis to rotation matrix 36 | Eigen::Matrix3d AngleAxisToRotation(const Eigen::Vector3d& aa); 37 | 38 | } // namespace glomap 39 | -------------------------------------------------------------------------------- /glomap/math/tree.cc: -------------------------------------------------------------------------------- 1 | #include "tree.h" 2 | 3 | // BGL includes 4 | #include 5 | #include 6 | 7 | namespace glomap { 8 | 9 | namespace { 10 | 11 | typedef boost::adjacency_list> 16 | weighted_graph; 17 | typedef boost::property_map::type 18 | weight_map; 19 | typedef boost::graph_traits::edge_descriptor edge_desc; 20 | typedef boost::graph_traits::vertex_descriptor vertex_desc; 21 | 22 | } // namespace 23 | 24 | // Function to perform breadth-first search (BFS) on a graph represented by an 25 | // adjacency list 26 | int BFS(const std::vector>& graph, 27 | int root, 28 | std::vector& parents, 29 | std::vector> banned_edges) { 30 | int num_vertices = graph.size(); 31 | 32 | // Create a vector to store the visited status of each vertex 33 | std::vector visited(num_vertices, false); 34 | 35 | // Create a vector to store the parent vertex for each vertex 36 | parents.clear(); 37 | parents.resize(num_vertices, -1); 38 | parents[root] = root; 39 | 40 | // Create a queue for BFS traversal 41 | std::queue q; 42 | 43 | // Mark the start vertex as visited and enqueue it 44 | visited[root] = true; 45 | q.push(root); 46 | 47 | int counter = 0; 48 | while (!q.empty()) { 49 | int current_vertex = q.front(); 50 | q.pop(); 51 | 52 | // Process the current vertex 53 | // Traverse the adjacent vertices 54 | for (int neighbor : graph[current_vertex]) { 55 | if (std::find(banned_edges.begin(), 56 | banned_edges.end(), 57 | std::make_pair(current_vertex, neighbor)) != 58 | banned_edges.end()) 59 | continue; 60 | if (std::find(banned_edges.begin(), 61 | banned_edges.end(), 62 | std::make_pair(neighbor, current_vertex)) != 63 | banned_edges.end()) 64 | continue; 65 | 66 | if (!visited[neighbor]) { 67 | visited[neighbor] = true; 68 | parents[neighbor] = current_vertex; 69 | q.push(neighbor); 70 | counter++; 71 | } 72 | } 73 | } 74 | 75 | return counter; 76 | } 77 | 78 | image_t MaximumSpanningTree(const ViewGraph& view_graph, 79 | const std::unordered_map& images, 80 | std::unordered_map& parents, 81 | WeightType type) { 82 | std::unordered_map image_id_to_idx; 83 | image_id_to_idx.reserve(images.size()); 84 | std::unordered_map idx_to_image_id; 85 | idx_to_image_id.reserve(images.size()); 86 | for (auto& [image_id, image] : images) { 87 | if (image.is_registered == false) continue; 88 | idx_to_image_id[image_id_to_idx.size()] = image_id; 89 | image_id_to_idx[image_id] = image_id_to_idx.size(); 90 | } 91 | 92 | double max_weight = 0; 93 | for (const auto& [pair_id, image_pair] : view_graph.image_pairs) { 94 | if (image_pair.is_valid == false) continue; 95 | if (type == INLIER_RATIO) 96 | max_weight = std::max(max_weight, image_pair.weight); 97 | else 98 | max_weight = 99 | std::max(max_weight, static_cast(image_pair.inliers.size())); 100 | } 101 | 102 | // establish graph 103 | weighted_graph G(image_id_to_idx.size()); 104 | weight_map weights_boost = boost::get(boost::edge_weight, G); 105 | 106 | edge_desc e; 107 | for (auto& [pair_id, image_pair] : view_graph.image_pairs) { 108 | if (image_pair.is_valid == false) continue; 109 | 110 | const Image& image1 = images.at(image_pair.image_id1); 111 | const Image& image2 = images.at(image_pair.image_id2); 112 | 113 | if (image1.is_registered == false || image2.is_registered == false) { 114 | continue; 115 | } 116 | 117 | int idx1 = image_id_to_idx[image_pair.image_id1]; 118 | int idx2 = image_id_to_idx[image_pair.image_id2]; 119 | 120 | // Set the weight to be negative, then the result would be a maximum 121 | // spanning tree 122 | e = boost::add_edge(idx1, idx2, G).first; 123 | if (type == INLIER_NUM) 124 | weights_boost[e] = max_weight - image_pair.inliers.size(); 125 | else if (type == INLIER_RATIO) 126 | weights_boost[e] = max_weight - image_pair.weight; 127 | else 128 | weights_boost[e] = max_weight - image_pair.inliers.size(); 129 | } 130 | 131 | std::vector 132 | mst; // vector to store MST edges (not a property map!) 133 | boost::kruskal_minimum_spanning_tree(G, std::back_inserter(mst)); 134 | 135 | std::vector> edges_list(image_id_to_idx.size()); 136 | for (const auto& edge : mst) { 137 | auto source = boost::source(edge, G); 138 | auto target = boost::target(edge, G); 139 | edges_list[source].push_back(target); 140 | edges_list[target].push_back(source); 141 | } 142 | 143 | std::vector parents_idx; 144 | BFS(edges_list, 0, parents_idx); 145 | 146 | // change the index back to image id 147 | parents.clear(); 148 | for (int i = 0; i < parents_idx.size(); i++) { 149 | parents[idx_to_image_id[i]] = idx_to_image_id[parents_idx[i]]; 150 | } 151 | 152 | return idx_to_image_id[0]; 153 | } 154 | 155 | }; // namespace glomap 156 | -------------------------------------------------------------------------------- /glomap/math/tree.h: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #include 5 | 6 | namespace glomap { 7 | enum WeightType { INLIER_NUM, INLIER_RATIO }; 8 | 9 | // Return the number of nodes in the tree 10 | int BFS(const std::vector>& graph, 11 | int root, 12 | std::vector& parents, 13 | std::vector> banned_edges = {}); 14 | 15 | image_t MaximumSpanningTree(const ViewGraph& view_graph, 16 | const std::unordered_map& images, 17 | std::unordered_map& parents, 18 | WeightType type); 19 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/math/two_view_geometry.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/math/two_view_geometry.h" 2 | 3 | namespace glomap { 4 | // Code from PoseLib by Viktor Larsson 5 | bool CheckCheirality(const Rigid3d& pose, 6 | const Eigen::Vector3d& x1, 7 | const Eigen::Vector3d& x2, 8 | double min_depth, 9 | double max_depth) { 10 | // This code assumes that x1 and x2 are unit vectors 11 | const Eigen::Vector3d Rx1 = pose.rotation * x1; 12 | 13 | // [1 a; a 1] * [lambda1; lambda2] = [b1; b2] 14 | // [lambda1; lambda2] = [1 s-a; -a 1] * [b1; b2] / (1 - a*a) 15 | const double a = -Rx1.dot(x2); 16 | const double b1 = -Rx1.dot(pose.translation); 17 | const double b2 = x2.dot(pose.translation); 18 | 19 | // Note that we drop the factor 1.0/(1-a*a) since it is always positive. 20 | const double lambda1 = b1 - a * b2; 21 | const double lambda2 = -a * b1 + b2; 22 | 23 | min_depth = min_depth * (1 - a * a); 24 | max_depth = max_depth * (1 - a * a); 25 | 26 | bool status = lambda1 > min_depth && lambda2 > min_depth; 27 | status = status && (lambda1 < max_depth) && (lambda2 < max_depth); 28 | return status; 29 | } 30 | 31 | // This code is from GC-RANSAC by Daniel Barath 32 | double GetOrientationSignum(const Eigen::Matrix3d& F, 33 | const Eigen::Vector3d& epipole, 34 | const Eigen::Vector2d& pt1, 35 | const Eigen::Vector2d& pt2) { 36 | double signum1 = F(0, 0) * pt2[0] + F(1, 0) * pt2[1] + F(2, 0); 37 | double signum2 = epipole(1) - epipole(2) * pt1[1]; 38 | return signum1 * signum2; 39 | } 40 | 41 | void EssentialFromMotion(const Rigid3d& pose, Eigen::Matrix3d* E) { 42 | *E << 0.0, -pose.translation(2), pose.translation(1), pose.translation(2), 43 | 0.0, -pose.translation(0), -pose.translation(1), pose.translation(0), 0.0; 44 | *E = (*E) * pose.rotation.toRotationMatrix(); 45 | } 46 | 47 | // Get the essential matrix from relative pose 48 | void FundamentalFromMotionAndCameras(const Camera& camera1, 49 | const Camera& camera2, 50 | const Rigid3d& pose, 51 | Eigen::Matrix3d* F) { 52 | Eigen::Matrix3d E; 53 | EssentialFromMotion(pose, &E); 54 | *F = camera2.GetK().transpose().inverse() * E * camera1.GetK().inverse(); 55 | } 56 | 57 | double SampsonError(const Eigen::Matrix3d& E, 58 | const Eigen::Vector2d& x1, 59 | const Eigen::Vector2d& x2) { 60 | Eigen::Vector3d Ex1 = E * x1.homogeneous(); 61 | Eigen::Vector3d Etx2 = E.transpose() * x2.homogeneous(); 62 | 63 | double C = Ex1.dot(x2.homogeneous()); 64 | double Cx = Ex1.head(2).squaredNorm(); 65 | double Cy = Etx2.head(2).squaredNorm(); 66 | double r2 = C * C / (Cx + Cy); 67 | 68 | return r2; 69 | } 70 | 71 | double SampsonError(const Eigen::Matrix3d& E, 72 | const Eigen::Vector3d& x1, 73 | const Eigen::Vector3d& x2) { 74 | Eigen::Vector3d Ex1 = E * x1 / (EPS + x1[2]); 75 | Eigen::Vector3d Etx2 = E.transpose() * x2 / (EPS + x2[2]); 76 | 77 | double C = Ex1.dot(x2); 78 | double Cx = Ex1.head(2).squaredNorm(); 79 | double Cy = Etx2.head(2).squaredNorm(); 80 | double r2 = C * C / (Cx + Cy); 81 | 82 | return r2; 83 | } 84 | 85 | double HomographyError(const Eigen::Matrix3d& H, 86 | const Eigen::Vector2d& x1, 87 | const Eigen::Vector2d& x2) { 88 | Eigen::Vector3d Hx1 = H * x1.homogeneous(); 89 | Eigen::Vector2d Hx1_norm = Hx1.head(2) / (EPS + Hx1[2]); 90 | double r2 = (Hx1_norm - x2).squaredNorm(); 91 | 92 | return r2; 93 | } 94 | 95 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/math/two_view_geometry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/camera.h" 4 | #include "glomap/scene/types.h" 5 | #include "glomap/types.h" 6 | 7 | namespace glomap { 8 | 9 | // Cheirality check for essential matrix 10 | bool CheckCheirality(const Rigid3d& pose, 11 | const Eigen::Vector3d& x1, 12 | const Eigen::Vector3d& x2, 13 | double min_depth = 0., 14 | double max_depth = 100.); 15 | 16 | // Get the orientation signum for fundamental matrix 17 | // For chierality check of fundamental matrix 18 | double GetOrientationSignum(const Eigen::Matrix3d& F, 19 | const Eigen::Vector3d& epipole, 20 | const Eigen::Vector2d& pt1, 21 | const Eigen::Vector2d& pt2); 22 | 23 | // Get the essential matrix from relative pose 24 | void EssentialFromMotion(const Rigid3d& pose, Eigen::Matrix3d* E); 25 | 26 | // Get the essential matrix from relative pose 27 | void FundamentalFromMotionAndCameras(const Camera& camera1, 28 | const Camera& camera2, 29 | const Rigid3d& pose, 30 | Eigen::Matrix3d* F); 31 | 32 | // Sampson error for the essential matrix 33 | // Input the normalized image coordinates (2d) 34 | double SampsonError(const Eigen::Matrix3d& E, 35 | const Eigen::Vector2d& x1, 36 | const Eigen::Vector2d& x2); 37 | 38 | // Sampson error for the essential matrix 39 | // Input the normalized image ray (3d) 40 | double SampsonError(const Eigen::Matrix3d& E, 41 | const Eigen::Vector3d& x1, 42 | const Eigen::Vector3d& x2); 43 | 44 | // Homography error for the homography matrix 45 | double HomographyError(const Eigen::Matrix3d& H, 46 | const Eigen::Vector2d& x1, 47 | const Eigen::Vector2d& x2); 48 | 49 | } // namespace glomap 50 | -------------------------------------------------------------------------------- /glomap/math/union_find.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace glomap { 6 | 7 | // UnionFind class to maintain disjoint sets for creating tracks 8 | template 9 | class UnionFind { 10 | public: 11 | // Find the root of the element x 12 | DataType Find(DataType x) { 13 | // If x is not in parent map, initialize it with x as its parent 14 | auto parentIt = parent_.find(x); 15 | if (parentIt == parent_.end()) { 16 | parent_.emplace_hint(parentIt, x, x); 17 | return x; 18 | } 19 | // Path compression: set the parent of x to the root of the set containing x 20 | if (parentIt->second != x) { 21 | parentIt->second = Find(parentIt->second); 22 | } 23 | return parentIt->second; 24 | } 25 | 26 | // Unite the sets containing x and y 27 | void Union(DataType x, DataType y) { 28 | DataType root_x = Find(x); 29 | DataType root_y = Find(y); 30 | if (root_x != root_y) parent_[root_x] = root_y; 31 | } 32 | 33 | void Clear() { parent_.clear(); } 34 | 35 | private: 36 | // Map to store the parent of each element 37 | std::unordered_map parent_; 38 | }; 39 | 40 | } // namespace glomap 41 | -------------------------------------------------------------------------------- /glomap/processors/image_pair_inliers.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/processors/image_pair_inliers.h" 2 | 3 | #include "glomap/math/two_view_geometry.h" 4 | 5 | namespace glomap { 6 | 7 | double ImagePairInliers::ScoreError() { 8 | // Count inliers base on the type 9 | if (image_pair.config == colmap::TwoViewGeometry::PLANAR || 10 | image_pair.config == colmap::TwoViewGeometry::PANORAMIC || 11 | image_pair.config == colmap::TwoViewGeometry::PLANAR_OR_PANORAMIC) 12 | return ScoreErrorHomography(); 13 | else if (image_pair.config == colmap::TwoViewGeometry::UNCALIBRATED) 14 | return ScoreErrorFundamental(); 15 | else if (image_pair.config == colmap::TwoViewGeometry::CALIBRATED) 16 | return ScoreErrorEssential(); 17 | return 0; 18 | } 19 | 20 | double ImagePairInliers::ScoreErrorEssential() { 21 | const Rigid3d& cam2_from_cam1 = image_pair.cam2_from_cam1; 22 | Eigen::Matrix3d E; 23 | EssentialFromMotion(cam2_from_cam1, &E); 24 | 25 | // eij = camera i on image j 26 | Eigen::Vector3d epipole12, epipole21; 27 | epipole12 = cam2_from_cam1.translation; 28 | epipole21 = Inverse(cam2_from_cam1).translation; 29 | 30 | if (epipole12[2] < 0) epipole12 = -epipole12; 31 | if (epipole21[2] < 0) epipole21 = -epipole21; 32 | 33 | if (image_pair.inliers.size() > 0) { 34 | image_pair.inliers.clear(); 35 | } 36 | 37 | image_t image_id1 = image_pair.image_id1; 38 | image_t image_id2 = image_pair.image_id2; 39 | 40 | double thres = options.max_epipolar_error_E; 41 | 42 | // Conver the threshold from pixel space to normalized space 43 | thres = options.max_epipolar_error_E * 0.5 * 44 | (1. / cameras->at(images.at(image_id1).camera_id).Focal() + 45 | 1. / cameras->at(images.at(image_id2).camera_id).Focal()); 46 | 47 | // Square the threshold for faster computation 48 | double sq_threshold = thres * thres; 49 | double score = 0.; 50 | Eigen::Vector3d pt1, pt2; 51 | 52 | // TODO: determine the best threshold for triangulation angle 53 | // double thres_angle = std::cos(DegToRad(1.)); 54 | double thres_epipole = std::cos(DegToRad(3.)); 55 | double thres_angle = 1; 56 | thres_angle += 1e-6; 57 | thres_epipole += 1e-6; 58 | for (size_t k = 0; k < image_pair.matches.rows(); ++k) { 59 | // Use the undistorted features 60 | pt1 = images.at(image_id1).features_undist[image_pair.matches(k, 0)]; 61 | pt2 = images.at(image_id2).features_undist[image_pair.matches(k, 1)]; 62 | const double r2 = SampsonError(E, pt1, pt2); 63 | 64 | if (r2 < sq_threshold) { 65 | bool cheirality = CheckCheirality(cam2_from_cam1, pt1, pt2, 1e-2, 100.); 66 | 67 | // Check whether two image rays have small triangulation angle or are too 68 | // close to the epipoles 69 | bool not_denegerate = true; 70 | 71 | // Check whether two image rays are too close 72 | double diff_angle = pt1.dot(cam2_from_cam1.rotation.inverse() * pt2); 73 | not_denegerate = (diff_angle < thres_angle); 74 | 75 | // Check whether two points are too close to the epipoles 76 | double diff_epipole1 = pt1.dot(epipole21); 77 | double diff_epipole2 = pt2.dot(epipole12); 78 | not_denegerate = not_denegerate && (diff_epipole1 < thres_epipole && 79 | diff_epipole2 < thres_epipole); 80 | 81 | if (cheirality && not_denegerate) { 82 | score += r2; 83 | image_pair.inliers.push_back(k); 84 | } else { 85 | score += sq_threshold; 86 | } 87 | } else { 88 | score += sq_threshold; 89 | } 90 | } 91 | return score; 92 | } 93 | 94 | double ImagePairInliers::ScoreErrorFundamental() { 95 | if (image_pair.inliers.size() > 0) { 96 | image_pair.inliers.clear(); 97 | } 98 | 99 | Eigen::Vector3d epipole = image_pair.F.row(0).cross(image_pair.F.row(2)); 100 | 101 | bool status = false; 102 | for (auto i = 0; i < 3; i++) { 103 | if ((epipole(i) > EPS) || (epipole(i) < -EPS)) { 104 | status = true; 105 | break; 106 | } 107 | } 108 | if (!status) { 109 | epipole = image_pair.F.row(1).cross(image_pair.F.row(2)); 110 | } 111 | 112 | // First, get the orientation signum for every point 113 | std::vector signums; 114 | int positive_count = 0; 115 | int negative_count = 0; 116 | 117 | image_t image_id1 = image_pair.image_id1; 118 | image_t image_id2 = image_pair.image_id2; 119 | 120 | double thres = options.max_epipolar_error_F; 121 | double sq_threshold = thres * thres; 122 | 123 | double score = 0.; 124 | Eigen::Vector2d pt1, pt2; 125 | 126 | std::vector inliers_pre; 127 | std::vector errors; 128 | for (size_t k = 0; k < image_pair.matches.rows(); ++k) { 129 | pt1 = images.at(image_id1).features[image_pair.matches(k, 0)]; 130 | pt2 = images.at(image_id2).features[image_pair.matches(k, 1)]; 131 | const double r2 = SampsonError(image_pair.F, pt1, pt2); 132 | 133 | if (r2 < sq_threshold) { 134 | signums.push_back(GetOrientationSignum(image_pair.F, epipole, pt1, pt2)); 135 | if (signums.back() > 0) { 136 | positive_count++; 137 | } else { 138 | negative_count++; 139 | } 140 | 141 | inliers_pre.push_back(k); 142 | errors.push_back(r2); 143 | } else { 144 | score += sq_threshold; 145 | } 146 | } 147 | bool is_positive = (positive_count > negative_count); 148 | 149 | // If cannot distinguish the signum, the pair should be invalid 150 | if (positive_count == negative_count) return 0; 151 | 152 | // Then, if the signum is not consistent with the cheirality, discard the 153 | // point 154 | for (int k = 0; k < inliers_pre.size(); k++) { 155 | bool cheirality = (signums[k] > 0) == is_positive; 156 | if (!cheirality) { 157 | score += sq_threshold; 158 | } else { 159 | image_pair.inliers.push_back(inliers_pre[k]); 160 | score += errors[k]; 161 | } 162 | } 163 | return score; 164 | } 165 | 166 | double ImagePairInliers::ScoreErrorHomography() { 167 | if (image_pair.inliers.size() > 0) { 168 | image_pair.inliers.clear(); 169 | } 170 | 171 | image_t image_id1 = image_pair.image_id1; 172 | image_t image_id2 = image_pair.image_id2; 173 | 174 | double thres = options.max_epipolar_error_H; 175 | double sq_threshold = thres * thres; 176 | double score = 0.; 177 | Eigen::Vector2d pt1, pt2; 178 | for (size_t k = 0; k < image_pair.matches.rows(); ++k) { 179 | pt1 = images.at(image_id1).features[image_pair.matches(k, 0)]; 180 | pt2 = images.at(image_id2).features[image_pair.matches(k, 1)]; 181 | const double r2 = HomographyError(image_pair.H, pt1, pt2); 182 | 183 | if (r2 < sq_threshold) { 184 | // TODO: cheirality check for homography. Is that a thing? 185 | bool cheirality = true; 186 | 187 | if (cheirality) { 188 | score += r2; 189 | image_pair.inliers.push_back(k); 190 | } else { 191 | score += sq_threshold; 192 | } 193 | } else { 194 | score += sq_threshold; 195 | } 196 | } 197 | return score; 198 | } 199 | 200 | void ImagePairsInlierCount(ViewGraph& view_graph, 201 | const std::unordered_map& cameras, 202 | const std::unordered_map& images, 203 | const InlierThresholdOptions& options, 204 | bool clean_inliers) { 205 | for (auto& [pair_id, image_pair] : view_graph.image_pairs) { 206 | if (!clean_inliers && image_pair.inliers.size() > 0) continue; 207 | image_pair.inliers.clear(); 208 | 209 | if (image_pair.is_valid == false) continue; 210 | ImagePairInliers inlier_finder(image_pair, images, options, &cameras); 211 | inlier_finder.ScoreError(); 212 | } 213 | } 214 | 215 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/processors/image_pair_inliers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/math/rigid3d.h" 4 | #include "glomap/scene/types_sfm.h" 5 | #include "glomap/types.h" 6 | 7 | namespace glomap { 8 | 9 | class ImagePairInliers { 10 | public: 11 | ImagePairInliers( 12 | ImagePair& image_pair, 13 | const std::unordered_map& images, 14 | const InlierThresholdOptions& options, 15 | const std::unordered_map* cameras = nullptr) 16 | : image_pair(image_pair), 17 | images(images), 18 | cameras(cameras), 19 | options(options) {} 20 | 21 | // use the sampson error and put the inlier result into the image pair 22 | double ScoreError(); 23 | 24 | protected: 25 | // Error for the case of essential matrix 26 | double ScoreErrorEssential(); 27 | 28 | // Error for the case of fundamental matrix 29 | double ScoreErrorFundamental(); 30 | 31 | // Error for the case of homography matrix 32 | double ScoreErrorHomography(); 33 | 34 | ImagePair& image_pair; 35 | const std::unordered_map& images; 36 | const std::unordered_map* cameras; 37 | const InlierThresholdOptions& options; 38 | }; 39 | 40 | void ImagePairsInlierCount(ViewGraph& view_graph, 41 | const std::unordered_map& cameras, 42 | const std::unordered_map& images, 43 | const InlierThresholdOptions& options, 44 | bool clean_inliers); 45 | 46 | } // namespace glomap 47 | -------------------------------------------------------------------------------- /glomap/processors/image_undistorter.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/processors/image_undistorter.h" 2 | 3 | #include 4 | 5 | namespace glomap { 6 | 7 | void UndistortImages(std::unordered_map& cameras, 8 | std::unordered_map& images, 9 | bool clean_points) { 10 | std::vector image_ids; 11 | for (auto& [image_id, image] : images) { 12 | const int num_points = image.features.size(); 13 | if (image.features_undist.size() == num_points && !clean_points) 14 | continue; // already undistorted 15 | image_ids.push_back(image_id); 16 | } 17 | 18 | colmap::ThreadPool thread_pool(colmap::ThreadPool::kMaxNumThreads); 19 | 20 | LOG(INFO) << "Undistorting images.."; 21 | const int num_images = image_ids.size(); 22 | for (int image_idx = 0; image_idx < num_images; image_idx++) { 23 | Image& image = images[image_ids[image_idx]]; 24 | const int num_points = image.features.size(); 25 | if (image.features_undist.size() == num_points && !clean_points) 26 | continue; // already undistorted 27 | 28 | const Camera& camera = cameras[image.camera_id]; 29 | 30 | thread_pool.AddTask([&image, &camera, num_points]() { 31 | image.features_undist.clear(); 32 | image.features_undist.reserve(num_points); 33 | for (int i = 0; i < num_points; i++) { 34 | image.features_undist.emplace_back( 35 | camera.CamFromImg(image.features[i]).homogeneous().normalized()); 36 | } 37 | }); 38 | } 39 | 40 | thread_pool.Wait(); 41 | LOG(INFO) << "Image undistortion done"; 42 | } 43 | 44 | } // namespace glomap 45 | -------------------------------------------------------------------------------- /glomap/processors/image_undistorter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/types_sfm.h" 4 | 5 | namespace glomap { 6 | 7 | void UndistortImages(std::unordered_map& cameras, 8 | std::unordered_map& images, 9 | bool clean_points = true); 10 | 11 | } // namespace glomap 12 | -------------------------------------------------------------------------------- /glomap/processors/reconstruction_normalizer.cc: -------------------------------------------------------------------------------- 1 | #include "reconstruction_normalizer.h" 2 | 3 | namespace glomap { 4 | 5 | colmap::Sim3d NormalizeReconstruction( 6 | std::unordered_map& cameras, 7 | std::unordered_map& images, 8 | std::unordered_map& tracks, 9 | bool fixed_scale, 10 | double extent, 11 | double p0, 12 | double p1) { 13 | // Coordinates of image centers or point locations. 14 | std::vector coords_x; 15 | std::vector coords_y; 16 | std::vector coords_z; 17 | 18 | coords_x.reserve(images.size()); 19 | coords_y.reserve(images.size()); 20 | coords_z.reserve(images.size()); 21 | for (const auto& [image_id, image] : images) { 22 | if (!image.is_registered) continue; 23 | const Eigen::Vector3d proj_center = image.Center(); 24 | coords_x.push_back(static_cast(proj_center(0))); 25 | coords_y.push_back(static_cast(proj_center(1))); 26 | coords_z.push_back(static_cast(proj_center(2))); 27 | } 28 | 29 | // Determine robust bounding box and mean. 30 | std::sort(coords_x.begin(), coords_x.end()); 31 | std::sort(coords_y.begin(), coords_y.end()); 32 | std::sort(coords_z.begin(), coords_z.end()); 33 | 34 | const size_t P0 = static_cast( 35 | (coords_x.size() > 3) ? p0 * (coords_x.size() - 1) : 0); 36 | const size_t P1 = static_cast( 37 | (coords_x.size() > 3) ? p1 * (coords_x.size() - 1) : coords_x.size() - 1); 38 | 39 | const Eigen::Vector3d bbox_min(coords_x[P0], coords_y[P0], coords_z[P0]); 40 | const Eigen::Vector3d bbox_max(coords_x[P1], coords_y[P1], coords_z[P1]); 41 | 42 | Eigen::Vector3d mean_coord(0, 0, 0); 43 | for (size_t i = P0; i <= P1; ++i) { 44 | mean_coord(0) += coords_x[i]; 45 | mean_coord(1) += coords_y[i]; 46 | mean_coord(2) += coords_z[i]; 47 | } 48 | mean_coord /= P1 - P0 + 1; 49 | 50 | // Calculate scale and translation, such that 51 | // translation is applied before scaling. 52 | double scale = 1.; 53 | if (!fixed_scale) { 54 | const double old_extent = (bbox_max - bbox_min).norm(); 55 | if (old_extent >= std::numeric_limits::epsilon()) { 56 | scale = extent / old_extent; 57 | } 58 | } 59 | colmap::Sim3d tform( 60 | scale, Eigen::Quaterniond::Identity(), -scale * mean_coord); 61 | 62 | for (auto& [_, image] : images) { 63 | if (image.is_registered) { 64 | image.cam_from_world = TransformCameraWorld(tform, image.cam_from_world); 65 | } 66 | } 67 | 68 | for (auto& [_, track] : tracks) { 69 | track.xyz = tform * track.xyz; 70 | } 71 | 72 | return tform; 73 | } 74 | 75 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/processors/reconstruction_normalizer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/types_sfm.h" 4 | 5 | #include "colmap/geometry/pose.h" 6 | 7 | namespace glomap { 8 | 9 | colmap::Sim3d NormalizeReconstruction( 10 | std::unordered_map& cameras, 11 | std::unordered_map& images, 12 | std::unordered_map& tracks, 13 | bool fixed_scale = false, 14 | double extent = 10., 15 | double p0 = 0.1, 16 | double p1 = 0.9); 17 | } // namespace glomap 18 | -------------------------------------------------------------------------------- /glomap/processors/reconstruction_pruning.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/processors/reconstruction_pruning.h" 2 | 3 | #include "glomap/processors/view_graph_manipulation.h" 4 | 5 | namespace glomap { 6 | image_t PruneWeaklyConnectedImages(std::unordered_map& images, 7 | std::unordered_map& tracks, 8 | int min_num_images, 9 | int min_num_observations) { 10 | // Prepare the 2d-3d correspondences 11 | std::unordered_map pair_covisibility_count; 12 | std::unordered_map image_observation_count; 13 | for (auto& [track_id, track] : tracks) { 14 | if (track.observations.size() <= 2) continue; 15 | 16 | for (size_t i = 0; i < track.observations.size(); i++) { 17 | image_observation_count[track.observations[i].first]++; 18 | for (size_t j = i + 1; j < track.observations.size(); j++) { 19 | image_t image_id1 = track.observations[i].first; 20 | image_t image_id2 = track.observations[j].first; 21 | if (image_id1 == image_id2) continue; 22 | image_pair_t pair_id = 23 | ImagePair::ImagePairToPairId(image_id1, image_id2); 24 | if (pair_covisibility_count.find(pair_id) == 25 | pair_covisibility_count.end()) { 26 | pair_covisibility_count[pair_id] = 1; 27 | } else { 28 | pair_covisibility_count[pair_id]++; 29 | } 30 | } 31 | } 32 | } 33 | 34 | // Establish the visibility graph 35 | size_t counter = 0; 36 | ViewGraph visibility_graph; 37 | std::vector pair_count; 38 | for (auto& [pair_id, count] : pair_covisibility_count) { 39 | // since the relative pose is only fixed if there are more than 5 points, 40 | // then require each pair to have at least 5 points 41 | if (count >= 5) { 42 | counter++; 43 | image_t image_id1, image_id2; 44 | ImagePair::PairIdToImagePair(pair_id, image_id1, image_id2); 45 | 46 | if (image_observation_count[image_id1] < min_num_observations || 47 | image_observation_count[image_id2] < min_num_observations) 48 | continue; 49 | 50 | visibility_graph.image_pairs.insert( 51 | std::make_pair(pair_id, ImagePair(image_id1, image_id2))); 52 | 53 | pair_count.push_back(count); 54 | visibility_graph.image_pairs[pair_id].is_valid = true; 55 | visibility_graph.image_pairs[pair_id].weight = count; 56 | } 57 | } 58 | LOG(INFO) << "Established visibility graph with " << counter << " pairs"; 59 | 60 | // sort the pair count 61 | std::sort(pair_count.begin(), pair_count.end()); 62 | double median_count = pair_count[pair_count.size() / 2]; 63 | 64 | // calculate the MAD (median absolute deviation) 65 | std::vector pair_count_diff(pair_count.size()); 66 | for (size_t i = 0; i < pair_count.size(); i++) { 67 | pair_count_diff[i] = std::abs(pair_count[i] - median_count); 68 | } 69 | std::sort(pair_count_diff.begin(), pair_count_diff.end()); 70 | double median_count_diff = pair_count_diff[pair_count_diff.size() / 2]; 71 | 72 | // The median 73 | LOG(INFO) << "Threshold for Strong Clustering: " 74 | << median_count - median_count_diff; 75 | 76 | return ViewGraphManipulater::EstablishStrongClusters( 77 | visibility_graph, 78 | images, 79 | ViewGraphManipulater::WEIGHT, 80 | std::max(median_count - median_count_diff, 20.), 81 | min_num_images); 82 | 83 | // return visibility_graph.MarkConnectedComponents(images, min_num_images); 84 | } 85 | 86 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/processors/reconstruction_pruning.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "glomap/scene/types_sfm.h" 5 | 6 | namespace glomap { 7 | 8 | image_t PruneWeaklyConnectedImages(std::unordered_map& images, 9 | std::unordered_map& tracks, 10 | int min_num_images = 2, 11 | int min_num_observations = 0); 12 | 13 | } // namespace glomap 14 | -------------------------------------------------------------------------------- /glomap/processors/relpose_filter.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/processors/relpose_filter.h" 2 | 3 | #include "glomap/math/rigid3d.h" 4 | 5 | namespace glomap { 6 | 7 | void RelPoseFilter::FilterRotations( 8 | ViewGraph& view_graph, 9 | const std::unordered_map& images, 10 | double max_angle) { 11 | int num_invalid = 0; 12 | for (auto& [pair_id, image_pair] : view_graph.image_pairs) { 13 | if (image_pair.is_valid == false) continue; 14 | 15 | const Image& image1 = images.at(image_pair.image_id1); 16 | const Image& image2 = images.at(image_pair.image_id2); 17 | 18 | if (image1.is_registered == false || image2.is_registered == false) { 19 | continue; 20 | } 21 | 22 | Rigid3d pose_calc = image2.cam_from_world * Inverse(image1.cam_from_world); 23 | 24 | double angle = CalcAngle(pose_calc, image_pair.cam2_from_cam1); 25 | if (angle > max_angle) { 26 | image_pair.is_valid = false; 27 | num_invalid++; 28 | } 29 | } 30 | 31 | LOG(INFO) << "Filtered " << num_invalid << " relative rotation with angle > " 32 | << max_angle << " degrees"; 33 | } 34 | 35 | void RelPoseFilter::FilterInlierNum(ViewGraph& view_graph, int min_inlier_num) { 36 | int num_invalid = 0; 37 | for (auto& [pair_id, image_pair] : view_graph.image_pairs) { 38 | if (image_pair.is_valid == false) continue; 39 | 40 | if (image_pair.inliers.size() < min_inlier_num) { 41 | image_pair.is_valid = false; 42 | num_invalid++; 43 | } 44 | } 45 | 46 | LOG(INFO) << "Filtered " << num_invalid 47 | << " relative poses with inlier number < " << min_inlier_num; 48 | } 49 | 50 | void RelPoseFilter::FilterInlierRatio(ViewGraph& view_graph, 51 | double min_inlier_ratio) { 52 | int num_invalid = 0; 53 | for (auto& [pair_id, image_pair] : view_graph.image_pairs) { 54 | if (image_pair.is_valid == false) continue; 55 | 56 | if (image_pair.inliers.size() / double(image_pair.matches.rows()) < 57 | min_inlier_ratio) { 58 | image_pair.is_valid = false; 59 | num_invalid++; 60 | } 61 | } 62 | 63 | LOG(INFO) << "Filtered " << num_invalid 64 | << " relative poses with inlier ratio < " << min_inlier_ratio; 65 | } 66 | 67 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/processors/relpose_filter.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "glomap/scene/types_sfm.h" 5 | 6 | namespace glomap { 7 | 8 | struct RelPoseFilter { 9 | // Filter relative pose based on rotation angle 10 | // max_angle: in degree 11 | static void FilterRotations(ViewGraph& view_graph, 12 | const std::unordered_map& images, 13 | double max_angle = 5.0); 14 | 15 | // Filter relative pose based on number of inliers 16 | // min_inlier_num: in degree 17 | static void FilterInlierNum(ViewGraph& view_graph, int min_inlier_num = 30); 18 | 19 | // Filter relative pose based on rate of inliers 20 | // min_weight: minimal ratio of inliers 21 | static void FilterInlierRatio(ViewGraph& view_graph, 22 | double min_inlier_ratio = 0.25); 23 | }; 24 | 25 | } // namespace glomap 26 | -------------------------------------------------------------------------------- /glomap/processors/track_filter.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/processors/track_filter.h" 2 | 3 | #include "glomap/math/rigid3d.h" 4 | 5 | namespace glomap { 6 | 7 | int TrackFilter::FilterTracksByReprojection( 8 | const ViewGraph& view_graph, 9 | const std::unordered_map& cameras, 10 | const std::unordered_map& images, 11 | std::unordered_map& tracks, 12 | double max_reprojection_error, 13 | bool in_normalized_image) { 14 | int counter = 0; 15 | for (auto& [track_id, track] : tracks) { 16 | std::vector observation_new; 17 | for (auto& [image_id, feature_id] : track.observations) { 18 | const Image& image = images.at(image_id); 19 | Eigen::Vector3d pt_calc = image.cam_from_world * track.xyz; 20 | if (pt_calc(2) < EPS) continue; 21 | 22 | double reprojection_error = max_reprojection_error; 23 | if (in_normalized_image) { 24 | const Eigen::Vector3d& feature_undist = 25 | image.features_undist.at(feature_id); 26 | 27 | Eigen::Vector2d pt_reproj = pt_calc.head(2) / pt_calc(2); 28 | reprojection_error = 29 | (pt_reproj - feature_undist.head(2) / (feature_undist(2) + EPS)) 30 | .norm(); 31 | } else { 32 | Eigen::Vector2d pt_reproj = pt_calc.head(2) / pt_calc(2); 33 | Eigen::Vector2d pt_dist; 34 | pt_dist = cameras.at(image.camera_id).ImgFromCam(pt_reproj); 35 | reprojection_error = (pt_dist - image.features.at(feature_id)).norm(); 36 | } 37 | 38 | // If the reprojection error is smaller than the threshold, then keep it 39 | if (reprojection_error < max_reprojection_error) { 40 | observation_new.emplace_back(image_id, feature_id); 41 | } 42 | } 43 | if (observation_new.size() != track.observations.size()) { 44 | counter++; 45 | track.observations = observation_new; 46 | } 47 | } 48 | LOG(INFO) << "Filtered " << counter << " / " << tracks.size() 49 | << " tracks by reprojection error"; 50 | return counter; 51 | } 52 | 53 | int TrackFilter::FilterTracksByAngle( 54 | const ViewGraph& view_graph, 55 | const std::unordered_map& cameras, 56 | const std::unordered_map& images, 57 | std::unordered_map& tracks, 58 | double max_angle_error) { 59 | int counter = 0; 60 | double thres = std::cos(DegToRad(max_angle_error)); 61 | double thres_uncalib = std::cos(DegToRad(max_angle_error * 2)); 62 | for (auto& [track_id, track] : tracks) { 63 | std::vector observation_new; 64 | for (auto& [image_id, feature_id] : track.observations) { 65 | const Image& image = images.at(image_id); 66 | // const Camera& camera = image.camera; 67 | const Eigen::Vector3d& feature_undist = 68 | image.features_undist.at(feature_id); 69 | Eigen::Vector3d pt_calc = image.cam_from_world * track.xyz; 70 | if (pt_calc(2) < EPS) continue; 71 | 72 | pt_calc = pt_calc.normalized(); 73 | double thres_cam = (cameras.at(image.camera_id).has_prior_focal_length) 74 | ? thres 75 | : thres_uncalib; 76 | 77 | if (pt_calc.dot(feature_undist) > thres_cam) { 78 | observation_new.emplace_back(std::make_pair(image_id, feature_id)); 79 | } 80 | } 81 | if (observation_new.size() != track.observations.size()) { 82 | counter++; 83 | track.observations = observation_new; 84 | } 85 | } 86 | LOG(INFO) << "Filtered " << counter << " / " << tracks.size() 87 | << " tracks by angle error"; 88 | return counter; 89 | } 90 | 91 | int TrackFilter::FilterTrackTriangulationAngle( 92 | const ViewGraph& view_graph, 93 | const std::unordered_map& images, 94 | std::unordered_map& tracks, 95 | double min_angle) { 96 | int counter = 0; 97 | double thres = std::cos(DegToRad(min_angle)); 98 | for (auto& [track_id, track] : tracks) { 99 | std::vector observation_new; 100 | std::vector pts_calc; 101 | pts_calc.reserve(track.observations.size()); 102 | for (auto& [image_id, feature_id] : track.observations) { 103 | const Image& image = images.at(image_id); 104 | Eigen::Vector3d pt_calc = (track.xyz - image.Center()).normalized(); 105 | pts_calc.emplace_back(pt_calc); 106 | } 107 | bool status = false; 108 | for (int i = 0; i < track.observations.size(); i++) { 109 | for (int j = i + 1; j < track.observations.size(); j++) { 110 | if (pts_calc[i].dot(pts_calc[j]) < thres) { 111 | status = true; 112 | break; 113 | } 114 | } 115 | } 116 | 117 | // If the triangulation angle is too small, just remove it 118 | if (!status) { 119 | counter++; 120 | track.observations.clear(); 121 | } 122 | } 123 | LOG(INFO) << "Filtered " << counter << " / " << tracks.size() 124 | << " tracks by too small triangulation angle"; 125 | return counter; 126 | } 127 | 128 | } // namespace glomap -------------------------------------------------------------------------------- /glomap/processors/track_filter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/types_sfm.h" 4 | 5 | namespace glomap { 6 | 7 | struct TrackFilter { 8 | static int FilterTracksByReprojection( 9 | const ViewGraph& view_graph, 10 | const std::unordered_map& cameras, 11 | const std::unordered_map& images, 12 | std::unordered_map& tracks, 13 | double max_reprojection_error = 1e-2, 14 | bool in_normalized_image = true); 15 | 16 | static int FilterTracksByAngle( 17 | const ViewGraph& view_graph, 18 | const std::unordered_map& cameras, 19 | const std::unordered_map& images, 20 | std::unordered_map& tracks, 21 | double max_angle_error = 1.); 22 | 23 | static int FilterTrackTriangulationAngle( 24 | const ViewGraph& view_graph, 25 | const std::unordered_map& images, 26 | std::unordered_map& tracks, 27 | double min_angle = 1.); 28 | }; 29 | 30 | } // namespace glomap 31 | -------------------------------------------------------------------------------- /glomap/processors/view_graph_manipulation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/types_sfm.h" 4 | 5 | namespace glomap { 6 | 7 | struct ViewGraphManipulater { 8 | enum StrongClusterCriteria { 9 | INLIER_NUM, 10 | WEIGHT, 11 | }; 12 | 13 | static image_pair_t SparsifyGraph(ViewGraph& view_graph, 14 | std::unordered_map& images, 15 | int expected_degree = 50); 16 | 17 | static image_t EstablishStrongClusters( 18 | ViewGraph& view_graph, 19 | std::unordered_map& images, 20 | StrongClusterCriteria criteria = INLIER_NUM, 21 | double min_thres = 100, // require strong edges 22 | int min_num_images = 2); 23 | 24 | static void UpdateImagePairsConfig( 25 | ViewGraph& view_graph, 26 | const std::unordered_map& cameras, 27 | const std::unordered_map& images); 28 | 29 | // Decompose the relative camera postion from the camera config 30 | static void DecomposeRelPose(ViewGraph& view_graph, 31 | std::unordered_map& cameras, 32 | std::unordered_map& images); 33 | }; 34 | 35 | } // namespace glomap 36 | -------------------------------------------------------------------------------- /glomap/scene/camera.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/types.h" 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | namespace glomap { 11 | 12 | struct Camera : public colmap::Camera { 13 | Camera() : colmap::Camera() {} 14 | Camera(const colmap::Camera& camera) : colmap::Camera(camera) {} 15 | 16 | Camera& operator=(const colmap::Camera& camera) { 17 | *this = Camera(camera); 18 | return *this; 19 | } 20 | 21 | bool has_refined_focal_length = false; 22 | 23 | inline double Focal() const; 24 | inline Eigen::Vector2d PrincipalPoint() const; 25 | inline Eigen::Matrix3d GetK() const; 26 | }; 27 | 28 | double Camera::Focal() const { return (FocalLengthX() + FocalLengthY()) / 2.0; } 29 | 30 | Eigen::Vector2d Camera::PrincipalPoint() const { 31 | return Eigen::Vector2d(PrincipalPointX(), PrincipalPointY()); 32 | } 33 | 34 | Eigen::Matrix3d Camera::GetK() const { 35 | Eigen::Matrix3d K; 36 | K << FocalLengthX(), 0, PrincipalPointX(), 0, FocalLengthY(), 37 | PrincipalPointY(), 0, 0, 1; 38 | return K; 39 | } 40 | 41 | inline poselib::Camera ColmapCameraToPoseLibCamera(const Camera& camera) { 42 | poselib::Camera pose_lib_camera( 43 | camera.ModelName(), camera.params, camera.width, camera.height); 44 | return pose_lib_camera; 45 | } 46 | 47 | } // namespace glomap 48 | -------------------------------------------------------------------------------- /glomap/scene/image.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/math/gravity.h" 4 | #include "glomap/scene/types.h" 5 | #include "glomap/types.h" 6 | 7 | namespace glomap { 8 | 9 | struct GravityInfo { 10 | public: 11 | // Whether the gravity information is available 12 | bool has_gravity = false; 13 | 14 | const Eigen::Matrix3d& GetRAlign() const { return R_align; } 15 | 16 | inline void SetGravity(const Eigen::Vector3d& g); 17 | inline Eigen::Vector3d GetGravity() const { return gravity; }; 18 | 19 | private: 20 | // Direction of the gravity 21 | Eigen::Vector3d gravity; 22 | 23 | // Alignment matrix, the second column is the gravity direction 24 | Eigen::Matrix3d R_align; 25 | }; 26 | 27 | struct Image { 28 | Image() : image_id(-1), file_name("") {} 29 | Image(image_t img_id, camera_t cam_id, std::string file_name) 30 | : image_id(img_id), file_name(file_name), camera_id(cam_id) {} 31 | 32 | // Basic information 33 | // image_id, file_name need to be specified at construction time 34 | const image_t image_id; 35 | const std::string file_name; 36 | 37 | // The id of the camera 38 | camera_t camera_id; 39 | 40 | // whether the image is within the largest connected component 41 | bool is_registered = false; 42 | int cluster_id = -1; 43 | 44 | // The pose of the image, defined as the transformation from world to camera. 45 | Rigid3d cam_from_world; 46 | 47 | // Gravity information 48 | GravityInfo gravity_info; 49 | 50 | // Distorted feature points in pixels. 51 | std::vector features; 52 | // Normalized feature rays, can be obtained by calling UndistortImages. 53 | std::vector features_undist; 54 | 55 | // Methods 56 | inline Eigen::Vector3d Center() const; 57 | }; 58 | 59 | Eigen::Vector3d Image::Center() const { 60 | return cam_from_world.rotation.inverse() * -cam_from_world.translation; 61 | } 62 | 63 | void GravityInfo::SetGravity(const Eigen::Vector3d& g) { 64 | gravity = g; 65 | R_align = GetAlignRot(g); 66 | has_gravity = true; 67 | } 68 | 69 | } // namespace glomap 70 | -------------------------------------------------------------------------------- /glomap/scene/image_pair.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/types.h" 4 | #include "glomap/types.h" 5 | 6 | #include 7 | 8 | #include 9 | 10 | namespace glomap { 11 | 12 | // FUTURE: add covariance to the relative pose 13 | struct ImagePair { 14 | ImagePair() : pair_id(-1), image_id1(-1), image_id2(-1) {} 15 | ImagePair(image_t img_id1, image_t img_id2, Rigid3d pose_rel = Rigid3d()) 16 | : pair_id(ImagePairToPairId(img_id1, img_id2)), 17 | image_id1(img_id1), 18 | image_id2(img_id2), 19 | cam2_from_cam1(pose_rel) {} 20 | 21 | // Ids are kept constant 22 | const image_pair_t pair_id; 23 | const image_t image_id1; 24 | const image_t image_id2; 25 | 26 | // indicator whether the image pair is valid 27 | bool is_valid = true; 28 | 29 | // weight is the initial inlier rate 30 | double weight = -1; 31 | 32 | // one of `ConfigurationType`. 33 | int config = colmap::TwoViewGeometry::UNDEFINED; 34 | 35 | // Essential matrix. 36 | Eigen::Matrix3d E = Eigen::Matrix3d::Zero(); 37 | // Fundamental matrix. 38 | Eigen::Matrix3d F = Eigen::Matrix3d::Zero(); 39 | // Homography matrix. 40 | Eigen::Matrix3d H = Eigen::Matrix3d::Zero(); 41 | 42 | // Relative pose. 43 | Rigid3d cam2_from_cam1; 44 | 45 | // Matches between the two images. 46 | // First column is the index of the feature in the first image. 47 | // Second column is the index of the feature in the second image. 48 | Eigen::MatrixXi matches; 49 | 50 | // Row index of inliers in the matches matrix. 51 | std::vector inliers; 52 | 53 | static inline image_pair_t ImagePairToPairId(const image_t image_id1, 54 | const image_t image_id2); 55 | 56 | static inline void PairIdToImagePair(const image_pair_t pair_id, 57 | image_t& image_id1, 58 | image_t& image_id2); 59 | }; 60 | 61 | image_pair_t ImagePair::ImagePairToPairId(const image_t image_id1, 62 | const image_t image_id2) { 63 | if (image_id1 > image_id2) { 64 | return static_cast(kMaxNumImages) * image_id2 + image_id1; 65 | } else { 66 | return static_cast(kMaxNumImages) * image_id1 + image_id2; 67 | } 68 | } 69 | 70 | void ImagePair::PairIdToImagePair(const image_pair_t pair_id, 71 | image_t& image_id1, 72 | image_t& image_id2) { 73 | image_id1 = static_cast(pair_id % kMaxNumImages); 74 | image_id2 = static_cast((pair_id - image_id1) / kMaxNumImages); 75 | } 76 | 77 | } // namespace glomap 78 | -------------------------------------------------------------------------------- /glomap/scene/track.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/types.h" 4 | 5 | #include 6 | 7 | #include 8 | 9 | namespace glomap { 10 | typedef std::pair Observation; 11 | 12 | struct Track { 13 | // The id of the track 14 | track_t track_id; 15 | 16 | // The 3D point of the track 17 | Eigen::Vector3d xyz = Eigen::Vector3d::Zero(); 18 | 19 | // The color of the track (now not used) 20 | Eigen::Vector3ub color = Eigen::Vector3ub::Zero(); 21 | 22 | // Whether the track has been estimated 23 | bool is_initialized = false; 24 | 25 | // The list where the track is observed (image_id, feature_id) 26 | std::vector observations; 27 | }; 28 | 29 | } // namespace glomap 30 | -------------------------------------------------------------------------------- /glomap/scene/types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace glomap { 13 | 14 | //////////////////////////////////////////////////////////////////////////////// 15 | // Index types, determines the maximum number of objects. 16 | //////////////////////////////////////////////////////////////////////////////// 17 | 18 | // Unique identifier for cameras. 19 | using colmap::camera_t; 20 | 21 | // Unique identifier for images. 22 | using colmap::image_t; 23 | 24 | // Each image pair gets a unique ID, see `Database::ImagePairToPairId`. 25 | typedef uint64_t image_pair_t; 26 | 27 | // Index per image, i.e. determines maximum number of 2D points per image. 28 | typedef uint32_t feature_t; 29 | 30 | // Unique identifier per added 3D point. Since we add many 3D points, 31 | // delete them, and possibly re-add them again, the maximum number of allowed 32 | // unique indices should be large. 33 | typedef uint64_t track_t; 34 | 35 | using colmap::Rigid3d; 36 | 37 | const image_t kMaxNumImages = colmap::Database::kMaxNumImages; 38 | const image_pair_t kInvalidImagePairId = -1; 39 | 40 | } // namespace glomap 41 | -------------------------------------------------------------------------------- /glomap/scene/types_sfm.h: -------------------------------------------------------------------------------- 1 | // This files contains all the necessary includes for sfm 2 | // Types defined by GLOMAP 3 | #include "glomap/scene/camera.h" 4 | #include "glomap/scene/image.h" 5 | #include "glomap/scene/track.h" 6 | #include "glomap/scene/types.h" 7 | #include "glomap/scene/view_graph.h" 8 | #include "glomap/types.h" 9 | 10 | // Standard libraries 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | -------------------------------------------------------------------------------- /glomap/scene/view_graph.cc: -------------------------------------------------------------------------------- 1 | #include "glomap/scene/view_graph.h" 2 | 3 | #include "glomap/math/union_find.h" 4 | 5 | #include 6 | 7 | namespace glomap { 8 | 9 | int ViewGraph::KeepLargestConnectedComponents( 10 | std::unordered_map& images) { 11 | EstablishAdjacencyList(); 12 | 13 | int num_comp = FindConnectedComponent(); 14 | 15 | int max_idx = -1; 16 | int max_img = 0; 17 | for (int comp = 0; comp < num_comp; comp++) { 18 | if (connected_components[comp].size() > max_img) { 19 | max_img = connected_components[comp].size(); 20 | max_idx = comp; 21 | } 22 | } 23 | 24 | if (max_img == 0) return 0; 25 | 26 | std::unordered_set largest_component = connected_components[max_idx]; 27 | 28 | // Set all images to not registered 29 | for (auto& [image_id, image] : images) image.is_registered = false; 30 | 31 | // Set the images in the largest component to registered 32 | for (auto image_id : largest_component) images[image_id].is_registered = true; 33 | 34 | // set all pairs not in the largest component to invalid 35 | num_pairs = 0; 36 | for (auto& [pair_id, image_pair] : image_pairs) { 37 | if (!images[image_pair.image_id1].is_registered || 38 | !images[image_pair.image_id2].is_registered) { 39 | image_pair.is_valid = false; 40 | } 41 | if (image_pair.is_valid) num_pairs++; 42 | } 43 | 44 | num_images = largest_component.size(); 45 | return max_img; 46 | } 47 | 48 | int ViewGraph::FindConnectedComponent() { 49 | connected_components.clear(); 50 | std::unordered_map visited; 51 | for (auto& [image_id, neighbors] : adjacency_list) { 52 | visited[image_id] = false; 53 | } 54 | 55 | for (auto& [image_id, neighbors] : adjacency_list) { 56 | if (!visited[image_id]) { 57 | std::unordered_set component; 58 | BFS(image_id, visited, component); 59 | connected_components.push_back(component); 60 | } 61 | } 62 | 63 | return connected_components.size(); 64 | } 65 | 66 | int ViewGraph::MarkConnectedComponents( 67 | std::unordered_map& images, int min_num_img) { 68 | EstablishAdjacencyList(); 69 | 70 | int num_comp = FindConnectedComponent(); 71 | 72 | std::vector> cluster_num_img(num_comp); 73 | for (int comp = 0; comp < num_comp; comp++) { 74 | cluster_num_img[comp] = 75 | std::make_pair(connected_components[comp].size(), comp); 76 | } 77 | std::sort(cluster_num_img.begin(), cluster_num_img.end(), std::greater<>()); 78 | 79 | // Set the cluster number of every image to be -1 80 | for (auto& [image_id, image] : images) image.cluster_id = -1; 81 | 82 | int comp = 0; 83 | for (; comp < num_comp; comp++) { 84 | if (cluster_num_img[comp].first < min_num_img) break; 85 | for (auto image_id : connected_components[cluster_num_img[comp].second]) 86 | images[image_id].cluster_id = comp; 87 | } 88 | 89 | return comp; 90 | } 91 | 92 | void ViewGraph::BFS(image_t root, 93 | std::unordered_map& visited, 94 | std::unordered_set& component) { 95 | std::queue q; 96 | q.push(root); 97 | visited[root] = true; 98 | component.insert(root); 99 | 100 | while (!q.empty()) { 101 | image_t curr = q.front(); 102 | q.pop(); 103 | 104 | for (image_t neighbor : adjacency_list[curr]) { 105 | if (!visited[neighbor]) { 106 | q.push(neighbor); 107 | visited[neighbor] = true; 108 | component.insert(neighbor); 109 | } 110 | } 111 | } 112 | } 113 | 114 | void ViewGraph::EstablishAdjacencyList() { 115 | adjacency_list.clear(); 116 | for (auto& [pair_id, image_pair] : image_pairs) { 117 | if (image_pair.is_valid) { 118 | adjacency_list[image_pair.image_id1].insert(image_pair.image_id2); 119 | adjacency_list[image_pair.image_id2].insert(image_pair.image_id1); 120 | } 121 | } 122 | } 123 | } // namespace glomap 124 | -------------------------------------------------------------------------------- /glomap/scene/view_graph.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "glomap/scene/camera.h" 4 | #include "glomap/scene/image.h" 5 | #include "glomap/scene/image_pair.h" 6 | #include "glomap/scene/types.h" 7 | #include "glomap/types.h" 8 | 9 | namespace glomap { 10 | 11 | class ViewGraph { 12 | public: 13 | // Methods 14 | inline void RemoveInvalidPair(image_pair_t pair_id); 15 | 16 | // Mark the image which is not connected to any other images as not registered 17 | // Return: the number of images in the largest connected component 18 | int KeepLargestConnectedComponents( 19 | std::unordered_map& images); 20 | 21 | // Mark the cluster of the cameras (cluster_id sort by the the number of 22 | // images) 23 | int MarkConnectedComponents(std::unordered_map& images, 24 | int min_num_img = -1); 25 | 26 | // Establish the adjacency list 27 | void EstablishAdjacencyList(); 28 | 29 | inline const std::unordered_map>& 30 | GetAdjacencyList() const; 31 | 32 | // Data 33 | std::unordered_map image_pairs; 34 | 35 | image_t num_images = 0; 36 | image_pair_t num_pairs = 0; 37 | 38 | private: 39 | int FindConnectedComponent(); 40 | 41 | void BFS(image_t root, 42 | std::unordered_map& visited, 43 | std::unordered_set& component); 44 | 45 | // Data for processing 46 | std::unordered_map> adjacency_list; 47 | std::vector> connected_components; 48 | }; 49 | 50 | const std::unordered_map>& 51 | ViewGraph::GetAdjacencyList() const { 52 | return adjacency_list; 53 | } 54 | 55 | void ViewGraph::RemoveInvalidPair(image_pair_t pair_id) { 56 | ImagePair& pair = image_pairs.at(pair_id); 57 | pair.is_valid = false; 58 | } 59 | 60 | } // namespace glomap 61 | -------------------------------------------------------------------------------- /glomap/types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | namespace glomap { 13 | 14 | constexpr double EPS = 1e-12; 15 | constexpr double HALF_PI = 3.141592653589793238462643383279502884L / 2; 16 | constexpr double TWO_PI = 2 * 3.141592653589793238462643383279502884L; 17 | 18 | struct InlierThresholdOptions { 19 | // Thresholds for 3D-2D matches 20 | double max_angle_error = 1.; // in degree, for global positioning 21 | double max_reprojection_error = 1e-2; // for bundle adjustment 22 | double min_triangulation_angle = 1.; // in degree, for triangulation 23 | 24 | // Thresholds for image_pair 25 | double max_epipolar_error_E = 1.; 26 | double max_epipolar_error_F = 4.; 27 | double max_epipolar_error_H = 4.; 28 | 29 | // Thresholds for edges 30 | double min_inlier_num = 30; 31 | double min_inlier_ratio = 0.25; 32 | double max_rotation_error = 10.; // in degree, for rotation averaging 33 | }; 34 | 35 | } // namespace glomap 36 | -------------------------------------------------------------------------------- /scripts/format/c++.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script applies clang-format to the whole repository. 4 | 5 | # Check version 6 | version_string=$(clang-format --version | sed -E 's/^.*(\d+\.\d+\.\d+-.*).*$/\1/') 7 | expected_version_string='19.1.0' 8 | if [[ "$version_string" =~ "$expected_version_string" ]]; then 9 | echo "clang-format version '$version_string' matches '$expected_version_string'" 10 | else 11 | echo "clang-format version '$version_string' doesn't match '$expected_version_string'" 12 | exit 1 13 | fi 14 | 15 | # Get all C++ files checked into the repo, excluding submodules 16 | root_folder=$(git rev-parse --show-toplevel) 17 | all_files=$( \ 18 | git ls-tree --full-tree -r --name-only HEAD . \ 19 | | grep "^glomap.*\(\.cc\|\.h\|\.hpp\|\.cpp\|\.cu\)$" \ 20 | | sed "s~^~$root_folder/~") 21 | num_files=$(echo $all_files | wc -w) 22 | echo "Formatting ${num_files} files" 23 | 24 | clang-format -i $all_files 25 | -------------------------------------------------------------------------------- /scripts/shell/enter_vs_dev_shell.ps1: -------------------------------------------------------------------------------- 1 | if (!$env:VisualStudioDevShell) { 2 | $vswhere = "${Env:ProgramFiles(x86)}/Microsoft Visual Studio/Installer/vswhere.exe" 3 | if (!(Test-Path $vswhere)) { 4 | throw "Failed to find vswhere.exe" 5 | } 6 | 7 | & $vswhere -latest -format json 8 | $vsInstance = & $vswhere -latest -format json | ConvertFrom-Json 9 | if ($LASTEXITCODE) { 10 | throw "vswhere.exe returned exit code $LASTEXITCODE" 11 | } 12 | 13 | Import-Module "$($vsInstance.installationPath)/Common7/Tools/Microsoft.VisualStudio.DevShell.dll" 14 | $prevCwd = Get-Location 15 | try { 16 | Enter-VsDevShell $vsInstance.instanceId -DevCmdArguments "-no_logo -host_arch=amd64 -arch=amd64" 17 | } catch { 18 | Write-Host $_ 19 | Write-Error "Failed to enter Visual Studio Dev Shell" 20 | exit 1 21 | } 22 | Set-Location $prevCwd 23 | 24 | $env:VisualStudioDevShell = $true 25 | } 26 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glomap", 3 | "description": "GLOMAP is a general purpose global structure-from-motion pipeline for image-based reconstruction. GLOMAP requires a COLMAP database as input and outputs a COLMAP sparse reconstruction. As compared to COLMAP, this project provides a much more efficient and scalable reconstruction process, typically 1-2 orders of magnitude faster, with on-par or superior reconstruction quality.", 4 | "homepage": "https://github.com/colmap/glomap", 5 | "license": "BSD-3-Clause", 6 | "supports": "(linux | (windows & !static) | osx) & (x86 | x64 | arm64)", 7 | "dependencies": [ 8 | "boost-algorithm", 9 | "boost-filesystem", 10 | "boost-graph", 11 | "boost-heap", 12 | "boost-program-options", 13 | "boost-property-map", 14 | "boost-property-tree", 15 | { 16 | "name": "ceres", 17 | "features": [ 18 | "lapack", 19 | "schur", 20 | "suitesparse" 21 | ] 22 | }, 23 | "eigen3", 24 | "flann", 25 | "freeimage", 26 | "gflags", 27 | "glog", 28 | { 29 | "name": "jasper", 30 | "default-features": false 31 | }, 32 | "metis", 33 | "sqlite3", 34 | { 35 | "name": "vcpkg-cmake", 36 | "host": true 37 | }, 38 | { 39 | "name": "vcpkg-cmake-config", 40 | "host": true 41 | }, 42 | "gtest", 43 | "suitesparse" 44 | ], 45 | "features": { 46 | "cuda": { 47 | "description": "Build with CUDA.", 48 | "dependencies": [ 49 | "glew", 50 | "cuda" 51 | ] 52 | } 53 | } 54 | } 55 | --------------------------------------------------------------------------------