diff --git a/.clang-tidy b/.clang-tidy
index 50b93153..fd1ab1b0 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -2,12 +2,14 @@ Checks: >
performance-*,
concurrency-*,
bugprone-*,
+ -clang-analyzer-security.ArrayBound,
-bugprone-easily-swappable-parameters,
-bugprone-exception-escape,
-bugprone-implicit-widening-of-multiplication-result,
-bugprone-narrowing-conversions,
-bugprone-reserved-identifier,
-bugprone-unchecked-optional-access,
+ -performance-enum-size,
cppcoreguidelines-virtual-class-destructor,
google-explicit-constructor,
google-build-using-namespace,
diff --git a/.github/workflows/install-ccache.ps1 b/.github/workflows/install-ccache.ps1
new file mode 100644
index 00000000..c9d15035
--- /dev/null
+++ b/.github/workflows/install-ccache.ps1
@@ -0,0 +1,38 @@
+[CmdletBinding()]
+param (
+ [Parameter(Mandatory = $true)]
+ [string] $Destination
+)
+
+$version = "4.10.2"
+$folder="ccache-$version-windows-x86_64"
+$url = "https://github.com/ccache/ccache/releases/download/v$version/$folder.zip"
+$expectedSha256 = "6252F081876A9A9F700FAE13A5AEC5D0D486B28261D7F1F72AC11C7AD9DF4DA9"
+
+$ErrorActionPreference = "Stop"
+
+try {
+ New-Item -Path "$Destination" -ItemType Container -ErrorAction SilentlyContinue
+
+ Write-Host "Download CCache"
+ $zipFilePath = Join-Path "$env:TEMP" "$folder.zip"
+ Invoke-WebRequest -Uri $url -UseBasicParsing -OutFile "$zipFilePath" -MaximumRetryCount 3
+
+ $hash = Get-FileHash $zipFilePath -Algorithm "sha256"
+ if ($hash.Hash -ne $expectedSha256) {
+ throw "File $Path hash $hash.Hash did not match expected hash $expectedHash"
+ }
+
+ Write-Host "Unzip CCache"
+ Expand-Archive -Path "$zipFilePath" -DestinationPath "$env:TEMP"
+
+ Write-Host "Move CCache"
+ Move-Item -Force "$env:TEMP/$folder/ccache.exe" "$Destination"
+ Remove-Item "$zipFilePath"
+ Remove-Item -Recurse "$env:TEMP/$folder"
+}
+catch {
+ Write-Host "Installation failed with an error"
+ $_.Exception | Format-List
+ exit -1
+}
diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml
new file mode 100644
index 00000000..a78a36de
--- /dev/null
+++ b/.github/workflows/mac.yml
@@ -0,0 +1,84 @@
+name: Mac
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [ assigned, opened, synchronize, reopened ]
+ release:
+ types: [ published, edited ]
+
+jobs:
+ build:
+ name: ${{ matrix.config.os }} ${{ matrix.config.arch }} ${{ matrix.config.cmakeBuildType }}
+ runs-on: ${{ matrix.config.os }}
+ strategy:
+ matrix:
+ config: [
+ {
+ os: macos-15,
+ arch: arm64,
+ cmakeBuildType: Release,
+ },
+ ]
+
+ env:
+ COMPILER_CACHE_VERSION: 1
+ COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache
+ CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache
+ CCACHE_BASEDIR: ${{ github.workspace }}
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/cache@v4
+ id: cache-builds
+ with:
+ key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ matrix.config.cmakeBuildType }}-${{ github.run_id }}-${{ github.run_number }}
+ restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ matrix.config.cmakeBuildType }}
+ path: ${{ env.COMPILER_CACHE_DIR }}
+
+ - name: Setup Mac
+ run: |
+ brew upgrade cmake || brew install cmake
+ brew install \
+ ninja \
+ boost \
+ eigen \
+ flann \
+ freeimage \
+ metis \
+ glog \
+ googletest \
+ ceres-solver \
+ qt5 \
+ glew \
+ cgal \
+ sqlite3 \
+ ccache
+ brew link --force libomp
+
+ - name: Configure and build
+ run: |
+ cmake --version
+ mkdir build
+ cd build
+ cmake .. \
+ -GNinja \
+ -DCMAKE_BUILD_TYPE=${{ matrix.config.cmakeBuildType }} \
+ -DTESTS_ENABLED=ON \
+ -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)"
+ ninja
+
+ - name: Run tests
+ run: |
+ cd build
+ set +e
+ ctest --output-on-failure -E .+colmap_.*
+
+ - name: Cleanup compiler cache
+ run: |
+ set -x
+ ccache --show-stats --verbose
+ ccache --evict-older-than 1d
+ ccache --show-stats --verbose
diff --git a/.github/workflows/ci.yml b/.github/workflows/ubuntu.yml
similarity index 81%
rename from .github/workflows/ci.yml
rename to .github/workflows/ubuntu.yml
index 888a383a..29d05f07 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ubuntu.yml
@@ -11,14 +11,22 @@ on:
jobs:
build:
- name: ${{ matrix.config.os }} ${{ matrix.config.cmakeBuildType }} ${{ matrix.config.cudaEnabled && 'CUDA' || '' }} ${{ matrix.config.asanEnabled && 'ASan' || '' }}
+ name: ${{ matrix.config.os }} ${{ matrix.config.cmakeBuildType }} ${{ matrix.config.cudaEnabled && 'CUDA' || '' }} ${{ matrix.config.asanEnabled && 'ASan' || '' }} ${{ matrix.config.coverageEnabled && 'Coverage' || '' }}
runs-on: ${{ matrix.config.os }}
strategy:
matrix:
config: [
-
+ {
+ os: ubuntu-24.04,
+ qtVersion: 6,
+ cmakeBuildType: RelWithDebInfo,
+ asanEnabled: false,
+ cudaEnabled: false,
+ checkCodeFormat: true,
+ },
{
os: ubuntu-22.04,
+ qtVersion: 6,
cmakeBuildType: Release,
asanEnabled: false,
cudaEnabled: false,
@@ -26,20 +34,23 @@ jobs:
},
{
os: ubuntu-22.04,
+ qtVersion: 5,
cmakeBuildType: Release,
asanEnabled: false,
cudaEnabled: true,
checkCodeFormat: false,
},
{
- os: ubuntu-22.04,
+ os: ubuntu-24.04,
+ qtVersion: 6,
cmakeBuildType: Release,
asanEnabled: true,
cudaEnabled: false,
checkCodeFormat: false,
},
{
- os: ubuntu-22.04,
+ os: ubuntu-24.04,
+ qtVersion: 6,
cmakeBuildType: ClangTidy,
asanEnabled: false,
cudaEnabled: false,
@@ -53,6 +64,8 @@ jobs:
CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache
CCACHE_BASEDIR: ${{ github.workspace }}
CTCACHE_DIR: ${{ github.workspace }}/compiler-cache/ctcache
+ GLOG_v: 2
+ GLOG_logtostderr: 1
steps:
- uses: actions/checkout@v4
@@ -62,14 +75,15 @@ jobs:
key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }}-${{ github.run_id }}-${{ github.run_number }}
restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }}
path: ${{ env.COMPILER_CACHE_DIR }}
-
- name: Install compiler cache
run: |
mkdir -p "$CCACHE_DIR" "$CTCACHE_DIR"
echo "$COMPILER_CACHE_DIR/bin" >> $GITHUB_PATH
+
if [ -f "$COMPILER_CACHE_DIR/bin/ccache" ]; then
exit 0
fi
+
set -x
wget https://github.com/ccache/ccache/releases/download/v4.8.2/ccache-4.8.2-linux-x86_64.tar.xz
echo "0b33f39766fe9db67f40418aed6a5b3d7b2f4f7fab025a8213264b77a2d0e1b1 ccache-4.8.2-linux-x86_64.tar.xz" | sha256sum --check
@@ -81,41 +95,50 @@ jobs:
echo "108b087f156a9fe7da0c796de1ef73f5855d2a33a27983769ea39061359a40fc ${ctcache_commit_id}.zip" | sha256sum --check
unzip "${ctcache_commit_id}.zip"
mv ctcache-${ctcache_commit_id}/clang-tidy* "$COMPILER_CACHE_DIR/bin"
+
- name: Check code format
if: matrix.config.checkCodeFormat
run: |
set +x -euo pipefail
- sudo apt-get update && sudo apt-get install -y clang-format-14 black
- ./scripts/format/clang_format.sh
+ python -m pip install clang-format==20.1.5
+ ./scripts/format/c++.sh
git diff --name-only
git diff --exit-code || (echo "Code formatting failed" && exit 1)
+
- name: Setup Ubuntu
run: |
+ if [ "${{ matrix.config.qtVersion }}" == "5" ]; then
+ qt_packages="qtbase5-dev libqt5opengl5-dev libcgal-qt5-dev"
+ elif [ "${{ matrix.config.qtVersion }}" == "6" ]; then
+ qt_packages="qt6-base-dev libqt6opengl6-dev libqt6openglwidgets6"
+ fi
+
sudo apt-get update && sudo apt-get install -y \
build-essential \
cmake \
ninja-build \
libboost-program-options-dev \
- libboost-filesystem-dev \
libboost-graph-dev \
libboost-system-dev \
libeigen3-dev \
- libsuitesparse-dev \
libceres-dev \
libflann-dev \
libfreeimage-dev \
libmetis-dev \
libgoogle-glog-dev \
libgtest-dev \
+ libgmock-dev \
libsqlite3-dev \
libglew-dev \
- qtbase5-dev \
+ $qt_packages \
libqt5opengl5-dev \
libcgal-dev \
libcgal-qt5-dev \
libgl1-mesa-dri \
libunwind-dev \
+ libcurl4-openssl-dev \
xvfb
+
if [ "${{ matrix.config.cudaEnabled }}" == "true" ]; then
if [ "${{ matrix.config.os }}" == "ubuntu-20.04" ]; then
sudo apt-get install -y \
@@ -133,16 +156,19 @@ jobs:
echo "CUDAHOSTCXX=/usr/bin/g++-10" >> $GITHUB_ENV
fi
fi
+
if [ "${{ matrix.config.asanEnabled }}" == "true" ]; then
- sudo apt-get install -y clang-15 libomp-15-dev
- echo "CC=/usr/bin/clang-15" >> $GITHUB_ENV
- echo "CXX=/usr/bin/clang++-15" >> $GITHUB_ENV
+ sudo apt-get install -y clang-18 libomp-18-dev
+ echo "CC=/usr/bin/clang-18" >> $GITHUB_ENV
+ echo "CXX=/usr/bin/clang++-18" >> $GITHUB_ENV
fi
+
if [ "${{ matrix.config.cmakeBuildType }}" == "ClangTidy" ]; then
- sudo apt-get install -y clang-15 clang-tidy-15 libomp-15-dev
- echo "CC=/usr/bin/clang-15" >> $GITHUB_ENV
- echo "CXX=/usr/bin/clang++-15" >> $GITHUB_ENV
+ sudo apt-get install -y clang-18 clang-tidy-18 libomp-18-dev
+ echo "CC=/usr/bin/clang-18" >> $GITHUB_ENV
+ echo "CXX=/usr/bin/clang++-18" >> $GITHUB_ENV
fi
+
- name: Upgrade CMake
run: |
CMAKE_VERSION=3.28.6
@@ -151,6 +177,7 @@ jobs:
tar -xzf ${CMAKE_DIR}.tar.gz
sudo cp -r ${CMAKE_DIR}/* /usr/local/
rm -rf ${CMAKE_DIR}*
+
- name: Configure and build
run: |
set -x
@@ -162,29 +189,31 @@ jobs:
-DCMAKE_BUILD_TYPE=${{ matrix.config.cmakeBuildType }} \
-DCMAKE_INSTALL_PREFIX=./install \
-DCMAKE_CUDA_ARCHITECTURES=50 \
- -DSuiteSparse_CHOLMOD_LIBRARY="/usr/lib/x86_64-linux-gnu/libcholmod.so" \
- -DSuiteSparse_CHOLMOD_INCLUDE_DIR="/usr/include/suitesparse" \
+ -DCUDA_ENABLED=${{ matrix.config.cudaEnabled }} \
-DTESTS_ENABLED=ON \
-DASAN_ENABLED=${{ matrix.config.asanEnabled }}
ninja -k 10000
+
- name: Run tests
if: ${{ matrix.config.cmakeBuildType != 'ClangTidy' }}
- run: |
+ run: |
export DISPLAY=":99.0"
export QT_QPA_PLATFORM="offscreen"
Xvfb :99 &
sleep 3
cd build
ctest --output-on-failure -E .+colmap_.*
+
- name: Cleanup compiler cache
run: |
set -x
ccache --show-stats --verbose
ccache --evict-older-than 1d
ccache --show-stats --verbose
+
echo "Size of ctcache before: $(du -sh $CTCACHE_DIR)"
echo "Number of ctcache files before: $(find $CTCACHE_DIR | wc -l)"
# Delete cache older than 10 days.
find "$CTCACHE_DIR"/*/ -mtime +10 -print0 | xargs -0 rm -rf
echo "Size of ctcache after: $(du -sh $CTCACHE_DIR)"
- echo "Number of ctcache files after: $(find $CTCACHE_DIR | wc -l)"
\ No newline at end of file
+ echo "Number of ctcache files after: $(find $CTCACHE_DIR | wc -l)"
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
new file mode 100644
index 00000000..3acfd2c6
--- /dev/null
+++ b/.github/workflows/windows.yml
@@ -0,0 +1,175 @@
+name: Windows
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [ assigned, opened, synchronize, reopened ]
+ release:
+ types: [ published, edited ]
+
+jobs:
+ build:
+ name: ${{ matrix.config.os }} ${{ matrix.config.cmakeBuildType }} ${{ matrix.config.cudaEnabled && 'CUDA' || '' }}
+ runs-on: ${{ matrix.config.os }}
+ strategy:
+ matrix:
+ config: [
+ {
+ os: windows-2025,
+ cmakeBuildType: Release,
+ cudaEnabled: false,
+ testsEnabled: true,
+ exportPackage: true,
+ },
+ ]
+
+ env:
+ COMPILER_CACHE_VERSION: 1
+ COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache
+ CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache
+ CCACHE_BASEDIR: ${{ github.workspace }}
+ VCPKG_COMMIT_ID: 2ad7bd06128280e02bfe02361d8ffd7d465cfcf0
+ GLOG_v: 1
+ GLOG_logtostderr: 1
+
+ steps:
+ - uses: actions/checkout@v4
+
+ # We define the vcpkg binary sources using separate variables for read and
+ # write operations:
+ # * Read sources are defined as inline. These can be read by anyone and,
+ # in particular, pull requests from forks. Unfortunately, we cannot
+ # define these as action environment variables. See:
+ # https://github.com/orgs/community/discussions/44322
+ # * Write sources are defined as action secret variables. These cannot be
+ # read by pull requests from forks but only from pull requests from
+ # within the target repository (i.e., created by a repository owner).
+ # This protects us from malicious actors accessing our secrets and
+ # gaining write access to our binary cache. For more information, see:
+ # https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
+ - name: Setup vcpkg binary cache
+ shell: pwsh
+ run: |
+ # !!!PLEASE!!! be nice and don't use this cache for your own purposes. This is only meant for CI purposes in this repository.
+ $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"
+ if ("${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }}") {
+ # The secrets are only accessible in runs triggered from within the target repository and not forks.
+ $VCPKG_BINARY_SOURCES += ";x-azblob,${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }},${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_SAS }},write"
+ }
+ echo "VCPKG_BINARY_SOURCES=${VCPKG_BINARY_SOURCES}" >> "${env:GITHUB_ENV}"
+
+ - name: Compiler cache
+ uses: actions/cache@v4
+ id: cache-builds
+ with:
+ key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }}-${{ github.run_id }}-${{ github.run_number }}
+ restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }}
+ path: ${{ env.COMPILER_CACHE_DIR }}
+
+ - name: Install ccache
+ shell: pwsh
+ run: |
+ New-Item -ItemType Directory -Force -Path "${{ env.CCACHE_DIR }}"
+ echo "${{ env.COMPILER_CACHE_DIR }}/bin" | Out-File -Encoding utf8 -Append -FilePath $env:GITHUB_PATH
+ if (Test-Path -PathType Leaf "${{ env.COMPILER_CACHE_DIR }}/bin/ccache.exe") {
+ exit
+ }
+ .github/workflows/install-ccache.ps1 -Destination "${{ env.COMPILER_CACHE_DIR }}/bin"
+
+ - name: Install CUDA
+ uses: Jimver/cuda-toolkit@v0.2.18
+ if: matrix.config.cudaEnabled
+ id: cuda-toolkit
+ with:
+ cuda: '12.6.2'
+ sub-packages: '["nvcc", "nvtx", "cudart", "curand", "curand_dev", "nvrtc_dev"]'
+ method: 'network'
+
+ - name: Setup vcpkg
+ shell: pwsh
+ run: |
+ ./scripts/shell/enter_vs_dev_shell.ps1
+ cd ${{ github.workspace }}
+ git clone https://github.com/microsoft/vcpkg
+ cd vcpkg
+ git reset --hard ${{ env.VCPKG_COMMIT_ID }}
+ ./bootstrap-vcpkg.bat
+
+ - name: Install CMake and Ninja
+ uses: lukka/get-cmake@latest
+ with:
+ cmakeVersion: "3.31.0"
+ ninjaVersion: "1.12.1"
+
+ - name: Configure and build
+ shell: pwsh
+ run: |
+ ./scripts/shell/enter_vs_dev_shell.ps1
+ cd ${{ github.workspace }}
+ ./vcpkg/vcpkg.exe integrate install
+ mkdir build
+ cd build
+ cmake .. `
+ -GNinja `
+ -DCMAKE_MAKE_PROGRAM=ninja `
+ -DCMAKE_BUILD_TYPE=Release `
+ -DTESTS_ENABLED=ON `
+ -DCUDA_ENABLED=${{ matrix.config.cudaEnabled }} `
+ -DGUI_ENABLED=OFF `
+ -DCGAL_ENABLED=OFF `
+ -DCMAKE_CUDA_ARCHITECTURES=all-major `
+ -DCUDAToolkit_ROOT="${{ steps.cuda-toolkit.outputs.CUDA_PATH }}" `
+ -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" `
+ -DVCPKG_TARGET_TRIPLET=x64-windows-release `
+ -DCMAKE_INSTALL_PREFIX=install
+ ninja
+
+ - name: Run tests
+ shell: pwsh
+ run: |
+ ./vcpkg/vcpkg.exe integrate install
+ cd build
+ ctest -E .+colmap_.* --output-on-failure
+
+ - name: Export package
+ if: matrix.config.exportPackage
+ shell: pwsh
+ run: |
+ ./vcpkg/vcpkg.exe integrate install
+
+ cd build
+ ninja install
+
+ ../vcpkg/vcpkg.exe install `
+ --triplet=x64-windows-release
+ $(if ($${{ matrix.config.cudaEnabled }}) { echo "--x-feature=cuda" })
+ ../vcpkg/vcpkg.exe export --raw --output-dir vcpkg_export --output glomap
+ cp vcpkg_export/glomap/installed/x64-windows/bin/*.dll install/bin
+ cp vcpkg_export/glomap/installed/x64-windows-release/bin/*.dll install/bin
+ if ($${{ matrix.config.cudaEnabled }}) {
+ cp "${{ steps.cuda-toolkit.outputs.CUDA_PATH }}/bin/cudart64_*.dll" install/bin
+ cp "${{ steps.cuda-toolkit.outputs.CUDA_PATH }}/bin/curand64_*.dll" install/bin
+ }
+
+ - name: Upload package
+ uses: actions/upload-artifact@v4
+ if: ${{ matrix.config.exportPackage && matrix.config.cudaEnabled }}
+ with:
+ name: glomap-x64-windows-cuda
+ path: build/install
+
+ - name: Upload package
+ uses: actions/upload-artifact@v4
+ if: ${{ matrix.config.exportPackage && !matrix.config.cudaEnabled }}
+ with:
+ name: glomap-x64-windows-nocuda
+ path: build/install
+
+ - name: Cleanup compiler cache
+ shell: pwsh
+ run: |
+ ccache --show-stats --verbose
+ ccache --evict-older-than 1d
+ ccache --show-stats --verbose
diff --git a/.gitignore b/.gitignore
index 55e9e67e..b88c9bff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,14 @@
-build
-data
+/build
+/data
.pixi
venv
*.mp4
-.vscode
+/.vscode
output
shell.nix
/run.sh
+/compile_commands.json
+.cache
# pixi environments
.pixi
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1cbeefa9..9eb97cb4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,20 +1,37 @@
cmake_minimum_required(VERSION 3.28)
-project(glomap VERSION 0.0.1)
+option(CUDA_ENABLED "Whether to enable CUDA, if available" ON)
+option(TESTS_ENABLED "Whether to build test binaries" OFF)
+option(ASAN_ENABLED "Whether to enable AddressSanitizer flags" OFF)
+option(CCACHE_ENABLED "Whether to enable compiler caching, if available" ON)
+option(FETCH_COLMAP "Whether to use COLMAP with FetchContent or with self-installed software" ON)
+option(FETCH_POSELIB "Whether to use PoseLib with FetchContent or with self-installed software" ON)
+
+# Propagate options to vcpkg manifest.
+if(CUDA_ENABLED)
+ list(APPEND VCPKG_MANIFEST_FEATURES "cuda")
+endif()
+
+# Initialize the project.
+project(glomap VERSION 1.1.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set_property(GLOBAL PROPERTY GLOBAL_DEPENDS_NO_CYCLES ON)
-option(OPENMP_ENABLED "Whether to enable OpenMP parallelization" ON)
-option(TESTS_ENABLED "Whether to build test binaries" OFF)
-option(ASAN_ENABLED "Whether to enable AddressSanitizer flags" OFF)
-option(CCACHE_ENABLED "Whether to enable compiler caching, if available" ON)
-option(FETCH_COLMAP "Whether to use COLMAP with FetchContent or with self-installed software" ON)
-option(FETCH_POSELIB "Whether to use PoseLib with FetchContent or with self-installed software" ON)
option(FETCH_RERUN "Whether to use Rerun with FetchContent or with self-installed software" ON)
+# Determine project compiler.
+if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
+ set(IS_MSVC TRUE)
+endif()
+if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+ set(IS_GNU TRUE)
+endif()
+if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
+ set(IS_CLANG TRUE)
+endif()
include(cmake/FindDependencies.cmake)
@@ -41,4 +58,21 @@ else()
message(STATUS "Disabling ccache support")
endif()
+if(IS_MSVC)
+ # Some fixes for the Glog library.
+ add_definitions("-DGLOG_USE_GLOG_EXPORT")
+ add_definitions("-DGLOG_NO_ABBREVIATED_SEVERITIES")
+ add_definitions("-DGL_GLEXT_PROTOTYPES")
+ add_definitions("-DNOMINMAX")
+ add_compile_options(/EHsc)
+ # Disable warning: 'initializing': conversion from 'X' to 'Y', possible loss of data
+ add_compile_options(/wd4244 /wd4267 /wd4305)
+ # Enable object level parallel builds in Visual Studio.
+ add_compile_options(/MP)
+ if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR "${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")
+ add_compile_options(/bigobj)
+ endif()
+endif()
+
+add_subdirectory(thirdparty)
add_subdirectory(glomap)
diff --git a/README.md b/README.md
index b65e3bd6..4475f801 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ If you use this project for your research, please cite
year={2024},
}
```
+To use the seperate rotation averaging module, refer to [this README](docs/rotation_averager.md).
## Using pixi
Easiest way to run this example is using pixi, you can easily install it using curl explained here
@@ -31,32 +32,52 @@ then
pixi run example
```
+## Installation
-## End-to-End with Docker.
+To build GLOMAP, first install [COLMAP](https://colmap.github.io/install.html#build-from-source)
+dependencies and then build GLOMAP using the following commands:
+```shell
+mkdir build
+cd build
+cmake .. -GNinja
+ninja && ninja install
+```
+Pre-compiled Windows binaries can be downloaded from the official
+[release page](https://github.com/colmap/glomap/releases).
-Download one of the [official datasets](https://colmap.github.io/datasets.html) unzip it.
-For example if we download the `south-building.zip`
-```bash
-mkdir -p data
-unzip south-building.zip -d data/
+After installation, one can run GLOMAP by (starting from a database)
+```shell
+glomap mapper --database_path DATABASE_PATH --output_path OUTPUT_PATH --image_path IMAGE_PATH
```
+For more details on the command line interface, one can type `glomap -h` or `glomap mapper -h` for help.
-Build and enter the docker container by running the following scripts:
-```bash
-cd docker
-./build.sh # Builds the docker image
-./start.sh # Starts the docker container
-./attach.sh # Enter the container
+We also provide a guide on improving the obtained reconstruction, which can be found [here](docs/getting_started.md).
-```
+Note:
+- GLOMAP depends on two external libraries - [COLMAP](https://github.com/colmap/colmap) and [PoseLib](https://github.com/PoseLib/PoseLib).
+ With the default setting, the library is built automatically by GLOMAP via `FetchContent`.
+ However, if a self-installed version is preferred, one can also disable the `FETCH_COLMAP` and `FETCH_POSELIB` CMake options.
+- 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.
+- If your system does not provide a recent enough CMake version, you can install it as:
+ ```shell
+ wget https://github.com/Kitware/CMake/releases/download/v3.30.1/cmake-3.30.1.tar.gz
+ tar xfvz cmake-3.30.1.tar.gz && cd cmake-3.30.1
+ ./bootstrap && make -j$(nproc) && sudo make install
+ ```
-When we're inside the container we can build `glomap` and install it.
-```bash
-mkdir -p build
-cd build
-cmake .. -GNinja
-ninja
-ninja install
+## End-to-End Example
+
+In this section, we will use datasets from [this link](https://demuc.de/colmap/datasets) as examples.
+Download the datasets and put them under `data` folder.
+
+### From database
+
+If a COLMAP database already exists, GLOMAP can directly use it to perform mapping:
+```shell
+glomap mapper \
+ --database_path ./data/gerrard-hall/database.db \
+ --image_path ./data/gerrard-hall/images \
+ --output_path ./output/gerrard-hall/sparse
```
To run it we must first extract the features and match them with `colmap`.
@@ -107,6 +128,22 @@ glomap mapper \
--output_path colosseum/sparse
```
+### Visualize and use the results
+
+The results are written out in the COLMAP sparse reconstruction format. Please
+refer to [COLMAP](https://colmap.github.io/format.html#sparse-reconstruction)
+for more details.
+
+The reconstruction can be visualized using the COLMAP GUI, for example:
+```shell
+colmap gui --import_path ./output/south-building/sparse/0
+```
+Alternatives like [rerun.io](https://rerun.io/examples/3d-reconstruction/glomap)
+also enable visualization of COLMAP and GLOMAP outputs.
+
+If you want to inspect the reconstruction programmatically, you can use
+`pycolmap` in Python or link against COLMAP's C++ library interface.
+
### Notes
- For larger scale datasets, it is recommended to use `sequential_matcher` or
diff --git a/cmake/FindDependencies.cmake b/cmake/FindDependencies.cmake
index 73b3ae85..7dc82b08 100644
--- a/cmake/FindDependencies.cmake
+++ b/cmake/FindDependencies.cmake
@@ -1,51 +1,97 @@
+set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+
include(FetchContent)
-FetchContent_Declare(PoseLib
- GIT_REPOSITORY https://github.com/PoseLib/PoseLib.git
- GIT_TAG b3691b791bcedccd5451621b2275a1df0d9dcdeb
- EXCLUDE_FROM_ALL
-)
-message(STATUS "Configuring PoseLib...")
-if (FETCH_POSELIB)
- FetchContent_MakeAvailable(PoseLib)
-else()
- find_package(PoseLib REQUIRED)
-endif()
-message(STATUS "Configuring PoseLib... done")
-
-FetchContent_Declare(COLMAP
- GIT_REPOSITORY https://github.com/colmap/colmap.git
- GIT_TAG 66fd8e56a0d160d68af2f29e9ac6941d442d2322
- EXCLUDE_FROM_ALL
-)
-message(STATUS "Configuring COLMAP...")
-set(UNINSTALL_ENABLED OFF CACHE INTERNAL "")
-if (FETCH_COLMAP)
- FetchContent_MakeAvailable(COLMAP)
-else()
- find_package(COLMAP REQUIRED)
-endif()
-message(STATUS "Configuring COLMAP... done")
message(STATUS "Configuring Rerun...")
if (FETCH_RERUN)
FetchContent_Declare(rerun_sdk URL
- https://github.com/rerun-io/rerun/releases/download/0.17.0/rerun_cpp_sdk.zip)
+ https://github.com/rerun-io/rerun/releases/download/0.28.1/rerun_cpp_sdk.zip)
FetchContent_MakeAvailable(rerun_sdk)
else()
find_package(rerun_sdk REQUIRED)
endif()
message(STATUS "Configuring Rerun... done")
-find_package(Eigen3 3.4 REQUIRED)
+find_package(Eigen3 REQUIRED)
find_package(Ceres REQUIRED COMPONENTS SuiteSparse)
find_package(Boost REQUIRED)
+find_package(OpenMP REQUIRED COMPONENTS C CXX)
+
+if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
+ find_package(Glog REQUIRED)
+endif()
+
+if(DEFINED glog_VERSION_MAJOR)
+ # Older versions of glog don't export version variables.
+ add_definitions("-DGLOG_VERSION_MAJOR=${glog_VERSION_MAJOR}")
+ add_definitions("-DGLOG_VERSION_MINOR=${glog_VERSION_MINOR}")
+endif()
if(TESTS_ENABLED)
message(STATUS "Enabling tests")
find_package(GTest REQUIRED)
endif()
-if (OPENMP_ENABLED)
- message(STATUS "Enabling OpenMP")
- find_package(OpenMP REQUIRED)
+set(CUDA_MIN_VERSION "7.0")
+if(CUDA_ENABLED)
+ if(CMAKE_VERSION VERSION_LESS 3.17)
+ find_package(CUDA QUIET)
+ if(CUDA_FOUND)
+ message(STATUS "Found CUDA version ${CUDA_VERSION} installed in "
+ "${CUDA_TOOLKIT_ROOT_DIR} via legacy CMake (<3.17) module. "
+ "Using the legacy CMake module means that any installation of "
+ "COLMAP will require that the CUDA libraries are "
+ "available under LD_LIBRARY_PATH.")
+ message(STATUS "Found CUDA ")
+ message(STATUS " Includes : ${CUDA_INCLUDE_DIRS}")
+ message(STATUS " Libraries : ${CUDA_LIBRARIES}")
+
+ enable_language(CUDA)
+
+ macro(declare_imported_cuda_target module)
+ add_library(CUDA::${module} INTERFACE IMPORTED)
+ target_include_directories(
+ CUDA::${module} INTERFACE ${CUDA_INCLUDE_DIRS})
+ target_link_libraries(
+ CUDA::${module} INTERFACE ${CUDA_${module}_LIBRARY} ${ARGN})
+ endmacro()
+
+ declare_imported_cuda_target(cudart ${CUDA_LIBRARIES})
+ declare_imported_cuda_target(curand ${CUDA_LIBRARIES})
+
+ set(CUDAToolkit_VERSION "${CUDA_VERSION_STRING}")
+ set(CUDAToolkit_BIN_DIR "${CUDA_TOOLKIT_ROOT_DIR}/bin")
+ else()
+ message(STATUS "CUDA not found")
+ endif()
+ else()
+ find_package(CUDAToolkit QUIET)
+ if(CUDAToolkit_FOUND)
+ set(CUDA_FOUND ON)
+ enable_language(CUDA)
+ else()
+ message(STATUS "CUDA not found")
+ endif()
+ endif()
+endif()
+
+if(CUDA_ENABLED AND CUDA_FOUND)
+ if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
+ set(CMAKE_CUDA_ARCHITECTURES "native")
+ endif()
+
+ add_definitions("-DGLOMAP_CUDA_ENABLED")
+
+ # Do not show warnings if the architectures are deprecated.
+ set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -Wno-deprecated-gpu-targets")
+ # Explicitly set PIC flags for CUDA targets.
+ if(NOT IS_MSVC)
+ set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --compiler-options -fPIC")
+ endif()
+
+ message(STATUS "Enabling CUDA support (version: ${CUDAToolkit_VERSION}, "
+ "archs: ${CMAKE_CUDA_ARCHITECTURES})")
+else()
+ set(CUDA_ENABLED OFF)
+ message(STATUS "Disabling CUDA support")
endif()
diff --git a/cmake/FindGlog.cmake b/cmake/FindGlog.cmake
new file mode 100644
index 00000000..8bf2ea49
--- /dev/null
+++ b/cmake/FindGlog.cmake
@@ -0,0 +1,118 @@
+# Copyright (c) 2023, ETH Zurich and UNC Chapel Hill.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
+# its contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+
+# Find package module for Glog library.
+#
+# The following variables are set by this module:
+#
+# GLOG_FOUND: TRUE if Glog is found.
+# glog::glog: Imported target to link against.
+#
+# The following variables control the behavior of this module:
+#
+# GLOG_INCLUDE_DIR_HINTS: List of additional directories in which to
+# search for Glog includes.
+# GLOG_LIBRARY_DIR_HINTS: List of additional directories in which to
+# search for Glog libraries.
+
+set(GLOG_INCLUDE_DIR_HINTS "" CACHE PATH "Glog include directory")
+set(GLOG_LIBRARY_DIR_HINTS "" CACHE PATH "Glog library directory")
+
+unset(GLOG_FOUND)
+
+find_package(glog CONFIG QUIET)
+if(TARGET glog::glog)
+ set(GLOG_FOUND TRUE)
+ message(STATUS "Found Glog")
+ message(STATUS " Target : glog::glog")
+else()
+ # Older versions of glog don't come with a find_package config.
+ # Fall back to custom logic to find the library and remap to imported target.
+
+ include(FindPackageHandleStandardArgs)
+
+ list(APPEND GLOG_CHECK_INCLUDE_DIRS
+ /usr/local/include
+ /usr/local/homebrew/include
+ /opt/local/var/macports/software
+ /opt/local/include
+ /usr/include)
+ list(APPEND GLOG_CHECK_PATH_SUFFIXES
+ glog/include
+ glog/Include
+ Glog/include
+ Glog/Include
+ src/windows)
+
+ list(APPEND GLOG_CHECK_LIBRARY_DIRS
+ /usr/local/lib
+ /usr/local/homebrew/lib
+ /opt/local/lib
+ /usr/lib)
+ list(APPEND GLOG_CHECK_LIBRARY_SUFFIXES
+ glog/lib
+ glog/Lib
+ Glog/lib
+ Glog/Lib
+ x64/Release)
+
+ find_path(GLOG_INCLUDE_DIRS
+ NAMES
+ glog/logging.h
+ PATHS
+ ${GLOG_INCLUDE_DIR_HINTS}
+ ${GLOG_CHECK_INCLUDE_DIRS}
+ PATH_SUFFIXES
+ ${GLOG_CHECK_PATH_SUFFIXES})
+ find_library(GLOG_LIBRARIES
+ NAMES
+ glog
+ libglog
+ PATHS
+ ${GLOG_LIBRARY_DIR_HINTS}
+ ${GLOG_CHECK_LIBRARY_DIRS}
+ PATH_SUFFIXES
+ ${GLOG_CHECK_LIBRARY_SUFFIXES})
+
+ if(GLOG_INCLUDE_DIRS AND GLOG_LIBRARIES)
+ set(GLOG_FOUND TRUE)
+ message(STATUS "Found Glog")
+ message(STATUS " Includes : ${GLOG_INCLUDE_DIRS}")
+ message(STATUS " Libraries : ${GLOG_LIBRARIES}")
+ endif()
+
+ add_library(glog::glog INTERFACE IMPORTED)
+ target_include_directories(glog::glog INTERFACE ${GLOG_INCLUDE_DIRS})
+ target_link_libraries(glog::glog INTERFACE ${GLOG_LIBRARIES})
+endif()
+
+if(NOT GLOG_FOUND AND GLOG_FIND_REQUIRED)
+ message(FATAL_ERROR "Could not find Glog")
+endif()
diff --git a/cmake/FindMETIS.cmake b/cmake/FindMETIS.cmake
new file mode 100644
index 00000000..5f41792d
--- /dev/null
+++ b/cmake/FindMETIS.cmake
@@ -0,0 +1,110 @@
+#
+# Copyright (c) 2022 Sergiu Deitsch
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTMETISLAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
+#[=======================================================================[.rst:
+Module for locating METIS
+=========================
+
+Read-only variables:
+
+``METIS_FOUND``
+ Indicates whether the library has been found.
+
+``METIS_VERSION``
+ Indicates library version.
+
+Targets
+-------
+
+``METIS::METIS``
+ Specifies targets that should be passed to target_link_libararies.
+]=======================================================================]
+
+include (FindPackageHandleStandardArgs)
+
+find_path (METIS_INCLUDE_DIR NAMES metis.h
+ PATH_SUFFIXES include
+ DOC "METIS include directory")
+find_library (METIS_LIBRARY_DEBUG NAMES metis
+ PATH_SUFFIXES Debug
+ DOC "METIS debug library")
+find_library (METIS_LIBRARY_RELEASE NAMES metis
+ PATH_SUFFIXES Release
+ DOC "METIS release library")
+
+if (METIS_LIBRARY_RELEASE)
+ if (METIS_LIBRARY_DEBUG)
+ set (METIS_LIBRARY debug ${METIS_LIBRARY_DEBUG} optimized
+ ${METIS_LIBRARY_RELEASE} CACHE STRING "METIS library")
+ else (METIS_LIBRARY_DEBUG)
+ set (METIS_LIBRARY ${METIS_LIBRARY_RELEASE} CACHE FILEPATH "METIS library")
+ endif (METIS_LIBRARY_DEBUG)
+elseif (METIS_LIBRARY_DEBUG)
+ set (METIS_LIBRARY ${METIS_LIBRARY_DEBUG} CACHE FILEPATH "METIS library")
+endif (METIS_LIBRARY_RELEASE)
+
+set (_METIS_VERSION_HEADER ${METIS_INCLUDE_DIR}/metis.h)
+
+if (EXISTS ${_METIS_VERSION_HEADER})
+ file (READ ${_METIS_VERSION_HEADER} _METIS_VERSION_CONTENTS)
+
+ string (REGEX REPLACE ".*#define METIS_VER_MAJOR[ \t]+([0-9]+).*" "\\1"
+ METIS_VERSION_MAJOR "${_METIS_VERSION_CONTENTS}")
+ string (REGEX REPLACE ".*#define METIS_VER_MINOR[ \t]+([0-9]+).*" "\\1"
+ METIS_VERSION_MINOR "${_METIS_VERSION_CONTENTS}")
+ string (REGEX REPLACE ".*#define METIS_VER_SUBMINOR[ \t]+([0-9]+).*" "\\1"
+ METIS_VERSION_PATCH "${_METIS_VERSION_CONTENTS}")
+
+ set (METIS_VERSION
+ ${METIS_VERSION_MAJOR}.${METIS_VERSION_MINOR}.${METIS_VERSION_PATCH})
+ set (METIS_VERSION_COMPONENTS 3)
+endif (EXISTS ${_METIS_VERSION_HEADER})
+
+mark_as_advanced (METIS_INCLUDE_DIR METIS_LIBRARY_DEBUG METIS_LIBRARY_RELEASE
+ METIS_LIBRARY)
+
+if (NOT TARGET METIS::METIS)
+ if (METIS_INCLUDE_DIR OR METIS_LIBRARY)
+ add_library (METIS::METIS IMPORTED UNKNOWN)
+ endif (METIS_INCLUDE_DIR OR METIS_LIBRARY)
+endif (NOT TARGET METIS::METIS)
+
+if (METIS_INCLUDE_DIR)
+ set_property (TARGET METIS::METIS PROPERTY INTERFACE_INCLUDE_DIRECTORIES
+ ${METIS_INCLUDE_DIR})
+endif (METIS_INCLUDE_DIR)
+
+if (METIS_LIBRARY_RELEASE)
+ set_property (TARGET METIS::METIS PROPERTY IMPORTED_LOCATION_RELEASE
+ ${METIS_LIBRARY_RELEASE})
+ set_property (TARGET METIS::METIS APPEND PROPERTY IMPORTED_CONFIGURATIONS
+ RELEASE)
+endif (METIS_LIBRARY_RELEASE)
+
+if (METIS_LIBRARY_DEBUG)
+ set_property (TARGET METIS::METIS PROPERTY IMPORTED_LOCATION_DEBUG
+ ${METIS_LIBRARY_DEBUG})
+ set_property (TARGET METIS::METIS APPEND PROPERTY IMPORTED_CONFIGURATIONS
+ DEBUG)
+endif (METIS_LIBRARY_DEBUG)
+
+find_package_handle_standard_args (METIS REQUIRED_VARS
+ METIS_INCLUDE_DIR METIS_LIBRARY VERSION_VAR METIS_VERSION)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index c81329b2..a56b2756 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,4 +1,4 @@
-FROM ubuntu:24.10
+FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends --no-install-suggests \
curl \
@@ -34,7 +34,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends --no-install-su
WORKDIR /ws/
-RUN pipx install rerun-sdk==0.17.0
+RUN pipx install rerun-sdk==0.28.1
ENV PATH=$PATH:/root/.local/share/pipx/venvs/rerun-sdk/bin/
# RUN ./install_colmap.sh
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 653b4364..a6399f56 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -1,20 +1,52 @@
# Getting started
-### Installation and End-to-End Examples
-Please refer to the main `README.md`
-### Recommended practice
+## Installation and end-to-end examples
+
+Please refer to the main `README.md`.
+
+## Recommended settings
+
The default parameters do not always gaurantee satisfying reconstructions.
-Regarding this, there are several things which can generally help
+Regarding this, there are several things which can generally help.
+
+### Share camera parameters
+
+If images are known to be taken with the same physical camera under identical
+camera settings, or images are well organized and known to be taken by several
+cameras, it is higly recommended to share the camera intrinsics as appropriate.
+To achieve this, one can set `--ImageReader.single_camera_per_folder` or
+`--ImageReader.single_camera_per_image` in `colmap feature_extractor`.
+
+### Handle high-resolution or blurry images
-#### Share camera parameters as much as possible
-If images are known to be taken with the same camera, or images are well organized and known to be taken by several cameras, it is higly recommended to share the camera intrinsics
-To achieve this, one can set `--ImageReader.single_camera_per_folder` or `--ImageReader.single_camera_per_image` in `colmap feature_extractor` to be 1.
+If images have high resolution or are blurry, it is worth trying to increase the
+allowed epipolar error by modifying `--RelPoseEstimation.max_epipolar_error`.
+For example, increase it to 4 or 10.
-#### Allow larger epipolar error
-If images are of high resolution, or are blurry, it is worth trying to increase the allowed epipolar error by modifying `--RelPoseEstimation.max_epipolar_error`. For example, make it 4, or 10.
+### Speedup reconstruction process
#### Cap the number of tracks
-If the number of images and points are large, the run-time of global bundle adjustment can be long. In this case, to further speed up the overall reconstruction process, the total number of points can be capped, by changing `--TrackEstablishment.max_num_tracks`. Typically, one image should not need more than 1000 tracks to achieve good performance, so this number can be adjusted to $1000 \times n$.
-Afterwards, if a full point cloud is desired (for example, to initialize a Gaussian Splatting), points can be triangulated directly by calling `colmap point_triangulator`.
-Note, if the `--skip_retriangulation` is not set true when calling `glomap mapper`, retriangulation should already been performed.
+If the number of images and points are large, the run-time of global bundle
+adjustment can be long. In this case, to further speed up the overall
+reconstruction process, the total number of points can be capped, by changing
+`--TrackEstablishment.max_num_tracks`. Typically, one image should not need more
+than 1000 tracks to achieve good performance, so this number can be adjusted to
+$1000 \times n$. Afterwards, if a full point cloud is desired (for example, to
+initialize a Gaussian Splatting), points can be triangulated directly by calling
+`colmap point_triangulator`.
+
+Note, if the `--skip_retriangulation` is not set when calling `glomap mapper`,
+retriangulation should already been performed.
+
+#### Limit optimization iterations
+
+The number of global positioning and bundle adjustment iterations can be limited
+using the `--GlobalPositioning.max_num_iterations` and
+`--BundleAdjustment.max_num_iterations` options.
+
+#### Enable GPU-based solver
+
+If Ceres 2.3 or above is installed and cuDSS is installed, GLOMAP supports GPU
+accelerated optimization. The process can be largely sped up with flags
+`--GlobalPositioning.use_gpu 1 --BundleAdjustment.use_gpu`.
diff --git a/docs/rotation_averager.md b/docs/rotation_averager.md
new file mode 100644
index 00000000..1fc342de
--- /dev/null
+++ b/docs/rotation_averager.md
@@ -0,0 +1,69 @@
+# Gravity-aligned Rotation Averaging with Circular Regression
+
+[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)
+---
+
+## About
+
+This project aims at solving the rotation averaging problem with gravity prior.
+To achieve this, circular regression is leveraged.
+
+If you use this project for your research, please cite
+```
+@inproceedings{pan2024ra1dof,
+ author={Pan, Linfei and Pollefeys, Marc and Barath, Daniel},
+ title={{Gravity-aligned Rotation Averaging with Circular Regression}},
+ booktitle={European Conference on Computer Vision (ECCV)},
+ year={2024},
+}
+```
+
+## Getting Started
+Install GLOMAP as instrcucted in [README](../README.md).
+Then, call the rotation averager (3 degree-of-freedom) via
+```
+glomap rotation_averager --relpose_path RELPOSE_PATH --output_path OUTPUT_PATH
+```
+
+If gravity directions are available, call the rotation averager (1 degree-of-freedom) via
+```
+glomap rotation_averager \
+ --relpose_path RELPOSE_PATH \
+ --output_path OUTPUT_PATH \
+ --gravity_path GRAVTIY PATH
+```
+It is recommended to set `--use_stratified=1` if only a subset of images have gravity direction.
+If gravity measurements are subject to i.i.d. noise, they can be refined by setting `--refine_gravity=1`.
+
+
+## File Formats
+### Relative Pose
+The relative pose file is expected to be of the following format
+```
+IMAGE_NAME_1 IMAGE_NAME_2 QW QX QY QZ TX TY TZ
+```
+Only images contained in at least one relative pose will be included in the following procedure.
+The relative pose should be 2R1 x1 + 2t1 = x2.
+
+### Gravity Direction
+The gravity direction file is expected to be of the following format
+```
+IMAGE_NAME GX GY GZ
+```
+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$.
+More explicitly, suppose we can transpose a 3D point from the world coordinate to the image coordinate by RX + t = x. Here:
+- `R` is a 3x3 rotation matrix that aligns the world coordinate system with the image coordinate system.
+- `X` is a 3D point in the world coordinate system.
+- `t` is a 3x1 translation vector that shifts the world coordinate system to the image coordinate system.
+- `x` is the corresponding 3D point in the image coordinate system.
+The gravity direction should be the second column of the rotation matrix `R`.
+
+It is acceptable if only a subset of all images have gravity direction.
+If the specified image name does not match any known image name from relative pose, it is ignored.
+
+### Output
+The estimated global rotation will be in the following format
+```
+IMAGE_NAME QW QX QY QZ
+```
+Any images that are not within the largest connected component of the view-graph formed by the relative pose will be ignored.
diff --git a/glomap/CMakeLists.txt b/glomap/CMakeLists.txt
index 856e20da..f73bd7f2 100644
--- a/glomap/CMakeLists.txt
+++ b/glomap/CMakeLists.txt
@@ -1,6 +1,7 @@
set(SOURCES
controllers/global_mapper.cc
controllers/option_manager.cc
+ controllers/rotation_averager.cc
controllers/track_establishment.cc
controllers/track_retriangulation.cc
estimators/bundle_adjustment.cc
@@ -8,10 +9,11 @@ set(SOURCES
estimators/global_rotation_averaging.cc
estimators/gravity_refinement.cc
estimators/relpose_estimation.cc
+ estimators/rotation_initializer.cc
estimators/view_graph_calibration.cc
io/colmap_converter.cc
io/colmap_io.cc
- io/gravity_io.cc
+ io/pose_io.cc
io/recording.cc
math/gravity.cc
math/rigid3d.cc
@@ -19,6 +21,7 @@ set(SOURCES
math/two_view_geometry.cc
processors/image_pair_inliers.cc
processors/image_undistorter.cc
+ processors/reconstruction_normalizer.cc
processors/reconstruction_pruning.cc
processors/relpose_filter.cc
processors/track_filter.cc
@@ -29,6 +32,7 @@ set(SOURCES
set(HEADERS
controllers/global_mapper.h
controllers/option_manager.h
+ controllers/rotation_averager.h
controllers/track_establishment.h
controllers/track_retriangulation.h
estimators/bundle_adjustment.h
@@ -36,26 +40,27 @@ set(HEADERS
estimators/global_positioning.h
estimators/global_rotation_averaging.h
estimators/gravity_refinement.h
- estimators/relpose_estimation.h
estimators/optimization_base.h
+ estimators/relpose_estimation.h
+ estimators/rotation_initializer.h
estimators/view_graph_calibration.h
io/colmap_converter.h
io/colmap_io.h
- io/gravity_io.h
+ io/pose_io.h
io/recording.h
math/gravity.h
- math/l1_solver.h
math/rigid3d.h
math/tree.h
math/two_view_geometry.h
- math/union_find.h
processors/image_pair_inliers.h
processors/image_undistorter.h
+ processors/reconstruction_normalizer.h
processors/reconstruction_pruning.h
processors/relpose_filter.h
processors/track_filter.h
processors/view_graph_manipulation.h
scene/camera.h
+ scene/frame.h
scene/image_pair.h
scene/image.h
scene/track.h
@@ -83,26 +88,16 @@ target_link_libraries(
PUBLIC
Eigen3::Eigen
Ceres::ceres
+ OpenMP::OpenMP_CXX
${BOOST_LIBRARIES}
- ${SuiteSparse_CHOLMOD_LIBRARY}
-)
-target_include_directories(
- glomap
- PUBLIC
- ..
- ${SuiteSparse_CHOLMOD_INCLUDE_DIR}
)
-
-if(OPENMP_FOUND)
- target_link_libraries(glomap PUBLIC OpenMP::OpenMP_CXX)
-endif()
+target_include_directories(glomap PUBLIC ..)
if(MSVC)
target_compile_options(glomap PRIVATE /bigobj)
else()
target_compile_options(glomap PRIVATE
-Wall
- -Werror
-Wno-sign-compare
-Wno-unused-variable
)
@@ -112,16 +107,19 @@ endif()
add_executable(glomap_main
glomap.cc
exe/global_mapper.h
- exe/global_mapper.cc)
+ exe/global_mapper.cc
+ exe/rotation_averager.h
+ exe/rotation_averager.cc
+)
target_link_libraries(glomap_main glomap)
set_target_properties(glomap_main PROPERTIES OUTPUT_NAME glomap)
install(TARGETS glomap_main DESTINATION bin)
-
if(TESTS_ENABLED)
add_executable(glomap_test
controllers/global_mapper_test.cc
+ controllers/rotation_averager_test.cc
)
target_link_libraries(
glomap_test
diff --git a/glomap/controllers/global_mapper.cc b/glomap/controllers/global_mapper.cc
index 75615780..c433f840 100644
--- a/glomap/controllers/global_mapper.cc
+++ b/glomap/controllers/global_mapper.cc
@@ -1,20 +1,27 @@
#include "global_mapper.h"
+#include "glomap/controllers/rotation_averager.h"
+#include "glomap/io/colmap_converter.h"
#include "glomap/processors/image_pair_inliers.h"
#include "glomap/processors/image_undistorter.h"
+#include "glomap/processors/reconstruction_normalizer.h"
#include "glomap/processors/reconstruction_pruning.h"
#include "glomap/processors/relpose_filter.h"
#include "glomap/processors/track_filter.h"
#include "glomap/processors/view_graph_manipulation.h"
#include "glomap/io/recording.h"
+#include
#include
namespace glomap {
+// TODO: Rig normalizaiton has not be done
bool GlobalMapper::Solve(const colmap::Database& database,
ViewGraph& view_graph,
+ std::unordered_map& rigs,
std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map& tracks) {
// 0. Preprocessing
@@ -44,6 +51,7 @@ bool GlobalMapper::Solve(const colmap::Database& database,
}
// 2. Run relative pose estimation
+ // TODO: Use generalized relative pose estimation for rigs.
if (!options_.skip_relative_pose_estimation) {
std::cout << "-------------------------------------" << std::endl;
std::cout << "Running relative pose estimation ..." << std::endl;
@@ -64,7 +72,10 @@ bool GlobalMapper::Solve(const colmap::Database& database,
RelPoseFilter::FilterInlierRatio(
view_graph, options_.inlier_thresholds.min_inlier_ratio);
- view_graph.KeepLargestConnectedComponents(images);
+ if (view_graph.KeepLargestConnectedComponents(frames, images) == 0) {
+ LOG(ERROR) << "no connected components are found";
+ return false;
+ }
run_timer.PrintSeconds();
}
@@ -78,21 +89,28 @@ bool GlobalMapper::Solve(const colmap::Database& database,
colmap::Timer run_timer;
run_timer.Start();
- RotationEstimator ra_engine(options_.opt_ra);
// The first run is for filtering
- ra_engine.EstimateRotations(view_graph, images);
+ SolveRotationAveraging(view_graph, rigs, frames, images, options_.opt_ra);
RelPoseFilter::FilterRotations(
view_graph, images, options_.inlier_thresholds.max_rotation_error);
- view_graph.KeepLargestConnectedComponents(images);
+ if (view_graph.KeepLargestConnectedComponents(frames, images) == 0) {
+ LOG(ERROR) << "no connected components are found";
+ return false;
+ }
// The second run is for final estimation
- if (!ra_engine.EstimateRotations(view_graph, images)) {
+ if (!SolveRotationAveraging(
+ view_graph, rigs, frames, images, options_.opt_ra)) {
return false;
}
RelPoseFilter::FilterRotations(
view_graph, images, options_.inlier_thresholds.max_rotation_error);
- image_t num_img = view_graph.KeepLargestConnectedComponents(images);
+ image_t num_img = view_graph.KeepLargestConnectedComponents(frames, images);
+ if (num_img == 0) {
+ LOG(ERROR) << "no connected components are found";
+ return false;
+ }
LOG(INFO) << num_img << " / " << images.size()
<< " images are within the connected component." << std::endl;
@@ -125,6 +143,12 @@ bool GlobalMapper::Solve(const colmap::Database& database,
std::cout << "Running global positioning ..." << std::endl;
std::cout << "-------------------------------------" << std::endl;
+ if (options_.opt_gp.constraint_type !=
+ GlobalPositionerOptions::ConstraintType::ONLY_POINTS) {
+ LOG(ERROR) << "Only points are used for solving camera positions";
+ return false;
+ }
+
colmap::Timer run_timer;
run_timer.Start();
// Undistort images in case all previous steps are skipped
@@ -134,24 +158,11 @@ bool GlobalMapper::Solve(const colmap::Database& database,
GlobalPositioner gp_engine(options_.opt_gp);
rr_rec.set_time_sequence("step", algorithm_step);
rr_rec.log("status", rerun::TextLog("Starting global positioning"));
- if (!gp_engine.Solve(view_graph, cameras, images, tracks)) {
- return false;
- }
- // If only camera-to-camera constraints are used for solving camera
- // positions, then points needs to be estimated separately
- if (options_.opt_gp.constraint_type ==
- GlobalPositionerOptions::ConstraintType::ONLY_CAMERAS) {
- GlobalPositionerOptions opt_gp_pt = options_.opt_gp;
- opt_gp_pt.constraint_type =
- GlobalPositionerOptions::ConstraintType::ONLY_POINTS;
- opt_gp_pt.optimize_positions = false;
- GlobalPositioner gp_engine_pt(opt_gp_pt);
- if (!gp_engine_pt.Solve(view_graph, cameras, images, tracks)) {
- return false;
- }
+ // TODO: consider to support other modes as well
+ if (!gp_engine.Solve(view_graph, rigs, cameras, frames, images, tracks)) {
+ return false;
}
-
// Filter tracks based on the estimation
TrackFilter::FilterTracksByAngle(
view_graph,
@@ -159,6 +170,24 @@ bool GlobalMapper::Solve(const colmap::Database& database,
images,
tracks,
options_.inlier_thresholds.max_angle_error);
+
+ // Filter tracks based on triangulation angle and reprojection error
+ TrackFilter::FilterTrackTriangulationAngle(
+ view_graph,
+ images,
+ tracks,
+ options_.inlier_thresholds.min_triangulation_angle);
+ // Set the threshold to be larger to avoid removing too many tracks
+ TrackFilter::FilterTracksByReprojection(
+ view_graph,
+ cameras,
+ images,
+ tracks,
+ 10 * options_.inlier_thresholds.max_reprojection_error);
+ // Normalize the structure
+ // If the camera rig is used, the structure do not need to be normalized
+ NormalizeReconstruction(rigs, cameras, frames, images, tracks);
+
run_timer.PrintSeconds();
rr_rec.set_time_sequence("step", algorithm_step++);
rr_rec.log("status", rerun::TextLog("Filter tracks based on the estimation"));
@@ -184,7 +213,7 @@ bool GlobalMapper::Solve(const colmap::Database& database,
// Staged bundle adjustment
// 6.1. First stage: optimize positions only
ba_engine_options_inner.optimize_rotations = false;
- if (!ba_engine.Solve(view_graph, cameras, images, tracks)) {
+ if (!ba_engine.Solve(rigs, cameras, frames, images, tracks)) {
return false;
}
LOG(INFO) << "Global bundle adjustment iteration " << ite + 1 << " / "
@@ -200,7 +229,7 @@ bool GlobalMapper::Solve(const colmap::Database& database,
ba_engine_options_inner.optimize_rotations =
options_.opt_ba.optimize_rotations;
if (ba_engine_options_inner.optimize_rotations &&
- !ba_engine.Solve(view_graph, cameras, images, tracks)) {
+ !ba_engine.Solve(rigs, cameras, frames, images, tracks)) {
return false;
}
LOG(INFO) << "Global bundle adjustment iteration " << ite + 1 << " / "
@@ -209,6 +238,9 @@ bool GlobalMapper::Solve(const colmap::Database& database,
if (ite != options_.num_iteration_bundle_adjustment - 1)
run_timer.PrintSeconds();
+ // Normalize the structure
+ NormalizeReconstruction(rigs, cameras, frames, images, tracks);
+
rr_rec.set_time_sequence("step", algorithm_step++);
rr_rec.log("status", rerun::TextLog("Global bundle adjustment iteration " + std::to_string(ite+1) + ", stage 2 finished"));
log_reconstruction(rr_rec, cameras, images, tracks);
@@ -277,8 +309,13 @@ bool GlobalMapper::Solve(const colmap::Database& database,
for (int ite = 0; ite < options_.num_iteration_retriangulation; ite++) {
colmap::Timer run_timer;
run_timer.Start();
- RetriangulateTracks(
- options_.opt_triangulator, database, cameras, images, tracks);
+ RetriangulateTracks(options_.opt_triangulator,
+ database,
+ rigs,
+ cameras,
+ frames,
+ images,
+ tracks);
run_timer.PrintSeconds();
std::cout << "-------------------------------------" << std::endl;
@@ -286,7 +323,7 @@ bool GlobalMapper::Solve(const colmap::Database& database,
std::cout << "-------------------------------------" << std::endl;
LOG(INFO) << "Bundle adjustment start" << std::endl;
BundleAdjuster ba_engine(options_.opt_ba);
- if (!ba_engine.Solve(view_graph, cameras, images, tracks)) {
+ if (!ba_engine.Solve(rigs, cameras, frames, images, tracks)) {
return false;
}
@@ -299,12 +336,15 @@ bool GlobalMapper::Solve(const colmap::Database& database,
images,
tracks,
options_.inlier_thresholds.max_reprojection_error);
- if (!ba_engine.Solve(view_graph, cameras, images, tracks)) {
+ if (!ba_engine.Solve(rigs, cameras, frames, images, tracks)) {
return false;
}
run_timer.PrintSeconds();
}
-
+
+ // Normalize the structure
+ NormalizeReconstruction(rigs, cameras, frames, images, tracks);
+
// Filter tracks based on the estimation
UndistortImages(cameras, images, true);
LOG(INFO) << "Filtering tracks by reprojection ...";
@@ -334,7 +374,7 @@ bool GlobalMapper::Solve(const colmap::Database& database,
run_timer.Start();
// Prune weakly connected images
- PruneWeaklyConnectedImages(images, tracks);
+ PruneWeaklyConnectedImages(frames, images, tracks);
run_timer.PrintSeconds();
}
@@ -346,4 +386,4 @@ bool GlobalMapper::Solve(const colmap::Database& database,
return true;
}
-} // namespace glomap
\ No newline at end of file
+} // namespace glomap
diff --git a/glomap/controllers/global_mapper.h b/glomap/controllers/global_mapper.h
index 79c1779f..4f4c3443 100644
--- a/glomap/controllers/global_mapper.h
+++ b/glomap/controllers/global_mapper.h
@@ -1,5 +1,4 @@
#pragma once
-
#include "glomap/controllers/track_establishment.h"
#include "glomap/controllers/track_retriangulation.h"
#include "glomap/estimators/bundle_adjustment.h"
@@ -42,13 +41,16 @@ struct GlobalMapperOptions {
bool skip_pruning = true;
};
+// TODO: Refactor the code to reuse the pipeline code more
class GlobalMapper {
public:
GlobalMapper(const GlobalMapperOptions& options) : options_(options) {}
bool Solve(const colmap::Database& database,
ViewGraph& view_graph,
+ std::unordered_map& rigs,
std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map& tracks);
diff --git a/glomap/controllers/global_mapper_test.cc b/glomap/controllers/global_mapper_test.cc
index 044c917e..98b8fc2b 100644
--- a/glomap/controllers/global_mapper_test.cc
+++ b/glomap/controllers/global_mapper_test.cc
@@ -40,13 +40,6 @@ void ExpectEqualReconstructions(const colmap::Reconstruction& gt,
GlobalMapperOptions CreateTestOptions() {
GlobalMapperOptions options;
- // Control the verbosity of the global sfm
- options.opt_vgcalib.verbose = false;
- options.opt_ra.verbose = false;
- options.opt_gp.verbose = false;
- options.opt_ba.verbose = false;
-
- // Control the flow of the global sfm
options.skip_view_graph_calibration = false;
options.skip_relative_pose_estimation = false;
options.skip_rotation_averaging = false;
@@ -60,28 +53,119 @@ GlobalMapperOptions CreateTestOptions() {
TEST(GlobalMapper, WithoutNoise) {
const std::string database_path = colmap::CreateTestDir() + "/database.db";
- colmap::Database database(database_path);
+ auto database = colmap::Database::Open(database_path);
+ colmap::Reconstruction gt_reconstruction;
+ colmap::SyntheticDatasetOptions synthetic_dataset_options;
+ synthetic_dataset_options.num_rigs = 2;
+ synthetic_dataset_options.num_cameras_per_rig = 1;
+ synthetic_dataset_options.num_frames_per_rig = 7;
+ synthetic_dataset_options.num_points3D = 50;
+ colmap::SynthesizeDataset(
+ synthetic_dataset_options, >_reconstruction, database.get());
+
+ ViewGraph view_graph;
+ std::unordered_map rigs;
+ std::unordered_map cameras;
+ std::unordered_map frames;
+ std::unordered_map images;
+ std::unordered_map tracks;
+
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
+
+ GlobalMapper global_mapper(CreateTestOptions());
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
+
+ colmap::Reconstruction reconstruction;
+ ConvertGlomapToColmap(rigs, cameras, frames, images, tracks, reconstruction);
+
+ ExpectEqualReconstructions(gt_reconstruction,
+ reconstruction,
+ /*max_rotation_error_deg=*/1e-2,
+ /*max_proj_center_error=*/1e-4,
+ /*num_obs_tolerance=*/0);
+}
+
+TEST(GlobalMapper, WithoutNoiseWithNonTrivialKnownRig) {
+ const std::string database_path = colmap::CreateTestDir() + "/database.db";
+
+ auto database = colmap::Database::Open(database_path);
+ colmap::Reconstruction gt_reconstruction;
+ colmap::SyntheticDatasetOptions synthetic_dataset_options;
+ synthetic_dataset_options.num_rigs = 2;
+ synthetic_dataset_options.num_cameras_per_rig = 2;
+ synthetic_dataset_options.num_frames_per_rig = 7;
+ synthetic_dataset_options.num_points3D = 50;
+ synthetic_dataset_options.sensor_from_rig_translation_stddev =
+ 0.1; // No noise
+ synthetic_dataset_options.sensor_from_rig_rotation_stddev = 5.; // No noise
+ colmap::SynthesizeDataset(
+ synthetic_dataset_options, >_reconstruction, database.get());
+
+ ViewGraph view_graph;
+ std::unordered_map rigs;
+ std::unordered_map cameras;
+ std::unordered_map frames;
+ std::unordered_map images;
+ std::unordered_map tracks;
+
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
+
+ GlobalMapper global_mapper(CreateTestOptions());
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
+
+ colmap::Reconstruction reconstruction;
+ ConvertGlomapToColmap(rigs, cameras, frames, images, tracks, reconstruction);
+
+ ExpectEqualReconstructions(gt_reconstruction,
+ reconstruction,
+ /*max_rotation_error_deg=*/1e-2,
+ /*max_proj_center_error=*/1e-4,
+ /*num_obs_tolerance=*/0);
+}
+
+TEST(GlobalMapper, WithoutNoiseWithNonTrivialUnknownRig) {
+ const std::string database_path = colmap::CreateTestDir() + "/database.db";
+
+ auto database = colmap::Database::Open(database_path);
colmap::Reconstruction gt_reconstruction;
colmap::SyntheticDatasetOptions synthetic_dataset_options;
- synthetic_dataset_options.num_cameras = 2;
- synthetic_dataset_options.num_images = 7;
+ synthetic_dataset_options.num_rigs = 2;
+ synthetic_dataset_options.num_cameras_per_rig = 3;
+ synthetic_dataset_options.num_frames_per_rig = 7;
synthetic_dataset_options.num_points3D = 50;
- synthetic_dataset_options.point2D_stddev = 0;
+ synthetic_dataset_options.sensor_from_rig_translation_stddev =
+ 0.1; // No noise
+ synthetic_dataset_options.sensor_from_rig_rotation_stddev = 5.; // No noise
+
colmap::SynthesizeDataset(
- synthetic_dataset_options, >_reconstruction, &database);
+ synthetic_dataset_options, >_reconstruction, database.get());
ViewGraph view_graph;
+ std::unordered_map rigs;
std::unordered_map cameras;
+ std::unordered_map frames;
std::unordered_map images;
std::unordered_map tracks;
- ConvertDatabaseToGlomap(database, view_graph, cameras, images);
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
+
+ // Set the rig sensors to be unknown
+ for (auto& [rig_id, rig] : rigs) {
+ for (auto& [sensor_id, sensor] : rig.NonRefSensors()) {
+ if (sensor.has_value()) {
+ rig.ResetSensorFromRig(sensor_id);
+ }
+ }
+ }
GlobalMapper global_mapper(CreateTestOptions());
- global_mapper.Solve(database, view_graph, cameras, images, tracks);
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
colmap::Reconstruction reconstruction;
- ConvertGlomapToColmap(cameras, images, tracks, reconstruction);
+ ConvertGlomapToColmap(rigs, cameras, frames, images, tracks, reconstruction);
ExpectEqualReconstructions(gt_reconstruction,
reconstruction,
@@ -93,29 +177,36 @@ TEST(GlobalMapper, WithoutNoise) {
TEST(GlobalMapper, WithNoiseAndOutliers) {
const std::string database_path = colmap::CreateTestDir() + "/database.db";
- colmap::Database database(database_path);
+ auto database = colmap::Database::Open(database_path);
colmap::Reconstruction gt_reconstruction;
colmap::SyntheticDatasetOptions synthetic_dataset_options;
- synthetic_dataset_options.num_cameras = 2;
- synthetic_dataset_options.num_images = 7;
+ synthetic_dataset_options.num_rigs = 2;
+ synthetic_dataset_options.num_cameras_per_rig = 1;
+ synthetic_dataset_options.num_frames_per_rig = 4;
synthetic_dataset_options.num_points3D = 100;
- synthetic_dataset_options.point2D_stddev = 0.5;
synthetic_dataset_options.inlier_match_ratio = 0.6;
colmap::SynthesizeDataset(
- synthetic_dataset_options, >_reconstruction, &database);
+ synthetic_dataset_options, >_reconstruction, database.get());
+ colmap::SyntheticNoiseOptions synthetic_noise_options;
+ synthetic_noise_options.point2D_stddev = 0.5;
+ colmap::SynthesizeNoise(
+ synthetic_noise_options, >_reconstruction, database.get());
ViewGraph view_graph;
std::unordered_map cameras;
+ std::unordered_map rigs;
std::unordered_map images;
+ std::unordered_map frames;
std::unordered_map tracks;
- ConvertDatabaseToGlomap(database, view_graph, cameras, images);
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
GlobalMapper global_mapper(CreateTestOptions());
- global_mapper.Solve(database, view_graph, cameras, images, tracks);
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
colmap::Reconstruction reconstruction;
- ConvertGlomapToColmap(cameras, images, tracks, reconstruction);
+ ConvertGlomapToColmap(rigs, cameras, frames, images, tracks, reconstruction);
ExpectEqualReconstructions(gt_reconstruction,
reconstruction,
diff --git a/glomap/controllers/option_manager.cc b/glomap/controllers/option_manager.cc
index 39c17f8f..6035cbfa 100644
--- a/glomap/controllers/option_manager.cc
+++ b/glomap/controllers/option_manager.cc
@@ -1,6 +1,7 @@
#include "option_manager.h"
#include "glomap/controllers/global_mapper.h"
+#include "glomap/estimators/gravity_refinement.h"
#include
#include
@@ -14,9 +15,13 @@ OptionManager::OptionManager(bool add_project_options) {
image_path = std::make_shared();
mapper = std::make_shared();
+ gravity_refiner = std::make_shared();
Reset();
desc_->add_options()("help,h", "");
+
+ AddAndRegisterDefaultOption("log_to_stderr", &FLAGS_logtostderr);
+ AddAndRegisterDefaultOption("log_level", &FLAGS_v);
}
void OptionManager::AddAllOptions() {
@@ -177,6 +182,10 @@ void OptionManager::AddGlobalPositionerOptions() {
return;
}
added_global_positioning_options_ = true;
+ AddAndRegisterDefaultOption("GlobalPositioning.use_gpu",
+ &mapper->opt_gp.use_gpu);
+ AddAndRegisterDefaultOption("GlobalPositioning.gpu_index",
+ &mapper->opt_gp.gpu_index);
AddAndRegisterDefaultOption("GlobalPositioning.optimize_positions",
&mapper->opt_gp.optimize_positions);
AddAndRegisterDefaultOption("GlobalPositioning.optimize_points",
@@ -196,12 +205,20 @@ void OptionManager::AddBundleAdjusterOptions() {
return;
}
added_bundle_adjustment_options_ = true;
+ AddAndRegisterDefaultOption("BundleAdjustment.use_gpu",
+ &mapper->opt_ba.use_gpu);
+ AddAndRegisterDefaultOption("BundleAdjustment.gpu_index",
+ &mapper->opt_ba.gpu_index);
+ AddAndRegisterDefaultOption("BundleAdjustment.optimize_rig_poses",
+ &mapper->opt_ba.optimize_rig_poses);
AddAndRegisterDefaultOption("BundleAdjustment.optimize_rotations",
&mapper->opt_ba.optimize_rotations);
AddAndRegisterDefaultOption("BundleAdjustment.optimize_translation",
&mapper->opt_ba.optimize_translation);
AddAndRegisterDefaultOption("BundleAdjustment.optimize_intrinsics",
&mapper->opt_ba.optimize_intrinsics);
+ AddAndRegisterDefaultOption("BundleAdjustment.optimize_principal_point",
+ &mapper->opt_ba.optimize_principal_point);
AddAndRegisterDefaultOption("BundleAdjustment.optimize_points",
&mapper->opt_ba.optimize_points);
AddAndRegisterDefaultOption("BundleAdjustment.thres_loss_function",
@@ -231,6 +248,14 @@ void OptionManager::AddInlierThresholdOptions() {
return;
}
added_inliers_options_ = true;
+ AddAndRegisterDefaultOption("Thresholds.max_angle_error",
+ &mapper->inlier_thresholds.max_angle_error);
+ AddAndRegisterDefaultOption(
+ "Thresholds.max_reprojection_error",
+ &mapper->inlier_thresholds.max_reprojection_error);
+ AddAndRegisterDefaultOption(
+ "Thresholds.min_triangulation_angle",
+ &mapper->inlier_thresholds.min_triangulation_angle);
AddAndRegisterDefaultOption("Thresholds.max_epipolar_error_E",
&mapper->inlier_thresholds.max_epipolar_error_E);
AddAndRegisterDefaultOption("Thresholds.max_epipolar_error_F",
@@ -245,6 +270,18 @@ void OptionManager::AddInlierThresholdOptions() {
&mapper->inlier_thresholds.max_rotation_error);
}
+void OptionManager::AddGravityRefinerOptions() {
+ if (added_gravity_refiner_options_) {
+ return;
+ }
+ added_gravity_refiner_options_ = true;
+ AddAndRegisterDefaultOption("GravityRefiner.max_outlier_ratio",
+ &gravity_refiner->max_outlier_ratio);
+ AddAndRegisterDefaultOption("GravityRefiner.max_gravity_error",
+ &gravity_refiner->max_gravity_error);
+ AddAndRegisterDefaultOption("GravityRefiner.min_num_neighbors",
+ &gravity_refiner->min_num_neighbors);
+}
void OptionManager::Reset() {
const bool kResetPaths = true;
ResetOptions(kResetPaths);
@@ -273,6 +310,7 @@ void OptionManager::ResetOptions(const bool reset_paths) {
*image_path = "";
}
*mapper = GlobalMapperOptions();
+ *gravity_refiner = GravityRefinerOptions();
}
void OptionManager::Parse(const int argc, char** argv) {
diff --git a/glomap/controllers/option_manager.h b/glomap/controllers/option_manager.h
index 0926387d..030d38e3 100644
--- a/glomap/controllers/option_manager.h
+++ b/glomap/controllers/option_manager.h
@@ -18,6 +18,7 @@ struct GlobalPositionerOptions;
struct BundleAdjusterOptions;
struct TriangulatorOptions;
struct InlierThresholdOptions;
+struct GravityRefinerOptions;
class OptionManager {
public:
@@ -37,6 +38,7 @@ class OptionManager {
void AddBundleAdjusterOptions();
void AddTriangulatorOptions();
void AddInlierThresholdOptions();
+ void AddGravityRefinerOptions();
template
void AddRequiredOption(const std::string& name,
@@ -56,6 +58,7 @@ class OptionManager {
std::shared_ptr image_path;
std::shared_ptr mapper;
+ std::shared_ptr gravity_refiner;
private:
template
@@ -88,6 +91,7 @@ class OptionManager {
bool added_bundle_adjustment_options_ = false;
bool added_triangulation_options_ = false;
bool added_inliers_options_ = false;
+ bool added_gravity_refiner_options_ = false;
};
template
diff --git a/glomap/controllers/rotation_averager.cc b/glomap/controllers/rotation_averager.cc
new file mode 100644
index 00000000..287e45dd
--- /dev/null
+++ b/glomap/controllers/rotation_averager.cc
@@ -0,0 +1,200 @@
+#include "glomap/controllers/rotation_averager.h"
+
+#include "glomap/estimators/rotation_initializer.h"
+#include "glomap/io/colmap_converter.h"
+
+namespace glomap {
+
+bool SolveRotationAveraging(ViewGraph& view_graph,
+ std::unordered_map& rigs,
+ std::unordered_map& frames,
+ std::unordered_map& images,
+ const RotationAveragerOptions& options) {
+ view_graph.KeepLargestConnectedComponents(frames, images);
+
+ bool solve_1dof_system = options.use_gravity && options.use_stratified;
+
+ ViewGraph view_graph_grav;
+ image_pair_t total_pairs = 0;
+ if (solve_1dof_system) {
+ // Prepare two sets: ones all with gravity, and one does not have gravity.
+ // Solve them separately first, then solve them in a single system
+ for (const auto& [pair_id, image_pair] : view_graph.image_pairs) {
+ if (!image_pair.is_valid) continue;
+
+ const Image& image1 = images[image_pair.image_id1];
+ const Image& image2 = images[image_pair.image_id2];
+
+ if (!image1.IsRegistered() || !image2.IsRegistered()) continue;
+
+ total_pairs++;
+
+ if (image1.HasGravity() && image2.HasGravity()) {
+ view_graph_grav.image_pairs.emplace(
+ pair_id,
+ ImagePair(image_pair.image_id1,
+ image_pair.image_id2,
+ image_pair.cam2_from_cam1));
+ }
+ }
+ }
+
+ const size_t grav_pairs = view_graph_grav.image_pairs.size();
+
+ LOG(INFO) << "Total image pairs: " << total_pairs
+ << ", gravity image pairs: " << grav_pairs;
+
+ // If there is no image pairs with gravity or most image pairs are with
+ // gravity, then just run the 3dof version
+ const bool status = grav_pairs == 0 || grav_pairs > total_pairs * 0.95;
+ solve_1dof_system = solve_1dof_system && !status;
+
+ if (solve_1dof_system) {
+ // Run the 1dof optimization
+ LOG(INFO) << "Solving subset 1DoF rotation averaging problem in the mixed "
+ "prior system";
+ view_graph_grav.KeepLargestConnectedComponents(frames, images);
+ RotationEstimator rotation_estimator_grav(options);
+ if (!rotation_estimator_grav.EstimateRotations(
+ view_graph_grav, rigs, frames, images)) {
+ return false;
+ }
+ view_graph.KeepLargestConnectedComponents(frames, images);
+ }
+
+ // By default, run trivial rotation averaging for cameras with unknown
+ // cam_from_rig.
+ std::unordered_set unknown_cams_from_rig;
+ rig_t max_rig_id = 0;
+ for (const auto& [rig_id, rig] : rigs) {
+ max_rig_id = std::max(max_rig_id, rig_id);
+ for (const auto& [sensor_id, sensor] : rig.NonRefSensors()) {
+ if (sensor_id.type != SensorType::CAMERA) continue;
+ if (!rig.MaybeSensorFromRig(sensor_id).has_value()) {
+ unknown_cams_from_rig.insert(sensor_id.id);
+ }
+ }
+ }
+
+ bool status_ra = false;
+ // If the trivial rotation averaging is enabled, run it
+ if (!unknown_cams_from_rig.empty() && !options.skip_initialization) {
+ LOG(INFO) << "Running trivial rotation averaging for rigged cameras";
+ // Create a rig for each camera
+ std::unordered_map rigs_trivial;
+ std::unordered_map frames_trivial;
+ std::unordered_map images_trivial;
+
+ // For cameras with known cam_from_rig, create rigs with only those sensors.
+ std::unordered_map camera_id_to_rig_id;
+ for (const auto& [rig_id, rig] : rigs) {
+ Rig rig_trivial;
+ rig_trivial.SetRigId(rig_id);
+ rig_trivial.AddRefSensor(rig.RefSensorId());
+ camera_id_to_rig_id[rig.RefSensorId().id] = rig_id;
+
+ for (const auto& [sensor_id, sensor] : rig.NonRefSensors()) {
+ if (sensor_id.type != SensorType::CAMERA) continue;
+ if (rig.MaybeSensorFromRig(sensor_id).has_value()) {
+ rig_trivial.AddSensor(sensor_id, sensor);
+ camera_id_to_rig_id[sensor_id.id] = rig_id;
+ }
+ }
+ rigs_trivial[rig_trivial.RigId()] = rig_trivial;
+ }
+
+ // For each camera with unknown cam_from_rig, create a separate trivial rig.
+ for (const auto& camera_id : unknown_cams_from_rig) {
+ Rig rig_trivial;
+ rig_trivial.SetRigId(++max_rig_id);
+ rig_trivial.AddRefSensor(sensor_t(SensorType::CAMERA, camera_id));
+ rigs_trivial[rig_trivial.RigId()] = rig_trivial;
+ camera_id_to_rig_id[camera_id] = rig_trivial.RigId();
+ }
+
+ frame_t max_frame_id = 0;
+ for (const auto& [frame_id, _] : frames) {
+ THROW_CHECK_NE(frame_id, colmap::kInvalidFrameId);
+ max_frame_id = std::max(max_frame_id, frame_id);
+ }
+ max_frame_id++;
+
+ for (auto& [frame_id, frame] : frames) {
+ Frame frame_trivial = Frame();
+ frame_trivial.SetFrameId(frame_id);
+ frame_trivial.SetRigId(frame.RigId());
+ frame_trivial.SetRigPtr(rigs_trivial.find(frame.RigId()) !=
+ rigs_trivial.end()
+ ? &rigs_trivial[frame.RigId()]
+ : nullptr);
+ frames_trivial[frame_id] = frame_trivial;
+
+ for (const auto& data_id : frame.ImageIds()) {
+ const auto& image = images.at(data_id.id);
+ if (!image.IsRegistered()) continue;
+ auto& image_trivial =
+ images_trivial
+ .emplace(data_id.id,
+ Image(data_id.id, image.camera_id, image.file_name))
+ .first->second;
+
+ if (unknown_cams_from_rig.find(image_trivial.camera_id) ==
+ unknown_cams_from_rig.end()) {
+ frames_trivial[frame_id].AddDataId(image_trivial.DataId());
+ image_trivial.frame_id = frame_id;
+ image_trivial.frame_ptr = &frames_trivial[frame_id];
+ } else {
+ // If the camera is not in any rig, then create a trivial frame
+ // for it
+ CreateFrameForImage(Rigid3d(),
+ image_trivial,
+ rigs_trivial,
+ frames_trivial,
+ camera_id_to_rig_id[image.camera_id],
+ max_frame_id);
+ max_frame_id++;
+ }
+ }
+ }
+
+ view_graph.KeepLargestConnectedComponents(frames_trivial, images_trivial);
+ // Run the trivial rotation averaging
+ RotationEstimatorOptions options_trivial = options;
+ options_trivial.skip_initialization = options.skip_initialization;
+ RotationEstimator rotation_estimator_trivial(options_trivial);
+ rotation_estimator_trivial.EstimateRotations(
+ view_graph, rigs_trivial, frames_trivial, images_trivial);
+
+ // Collect the results
+ std::unordered_map cams_from_world;
+ for (const auto& [image_id, image] : images_trivial) {
+ if (!image.IsRegistered()) continue;
+ cams_from_world[image_id] = image.CamFromWorld();
+ }
+
+ ConvertRotationsFromImageToRig(cams_from_world, images, rigs, frames);
+
+ RotationEstimatorOptions options_ra = options;
+ options_ra.skip_initialization = true;
+ RotationEstimator rotation_estimator(options_ra);
+ status_ra =
+ rotation_estimator.EstimateRotations(view_graph, rigs, frames, images);
+ view_graph.KeepLargestConnectedComponents(frames, images);
+ } else {
+ RotationAveragerOptions options_ra = options;
+ // For cases where there are some cameras without known cam_from_rig
+ // transformation, we need to run the rotation averaging with the
+ // skip_initialization flag set to false for convergence
+ if (unknown_cams_from_rig.size() > 0) {
+ options_ra.skip_initialization = false;
+ }
+
+ RotationEstimator rotation_estimator(options_ra);
+ status_ra =
+ rotation_estimator.EstimateRotations(view_graph, rigs, frames, images);
+ view_graph.KeepLargestConnectedComponents(frames, images);
+ }
+ return status_ra;
+}
+
+} // namespace glomap
diff --git a/glomap/controllers/rotation_averager.h b/glomap/controllers/rotation_averager.h
new file mode 100644
index 00000000..1ba93102
--- /dev/null
+++ b/glomap/controllers/rotation_averager.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "glomap/estimators/global_rotation_averaging.h"
+
+namespace glomap {
+
+struct RotationAveragerOptions : public RotationEstimatorOptions {
+ RotationAveragerOptions() = default;
+ RotationAveragerOptions(const RotationEstimatorOptions& options)
+ : RotationEstimatorOptions(options) {}
+ bool use_stratified = true;
+};
+
+bool SolveRotationAveraging(ViewGraph& view_graph,
+ std::unordered_map& rigs,
+ std::unordered_map& frames,
+ std::unordered_map& images,
+ const RotationAveragerOptions& options);
+
+} // namespace glomap
\ No newline at end of file
diff --git a/glomap/controllers/rotation_averager_test.cc b/glomap/controllers/rotation_averager_test.cc
new file mode 100644
index 00000000..1dac8abd
--- /dev/null
+++ b/glomap/controllers/rotation_averager_test.cc
@@ -0,0 +1,453 @@
+#include "glomap/controllers/rotation_averager.h"
+
+#include "glomap/controllers/global_mapper.h"
+#include "glomap/estimators/gravity_refinement.h"
+#include "glomap/io/colmap_io.h"
+#include "glomap/math/rigid3d.h"
+#include "glomap/types.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+namespace glomap {
+namespace {
+
+void CreateRandomRotation(const double stddev, Eigen::Quaterniond& q) {
+ std::random_device rd{};
+ std::mt19937 gen{rd()};
+
+ // Construct a random axis
+ double theta = colmap::RandomUniformReal(0, 2 * M_PI);
+ double phi = colmap::RandomUniformReal(0, M_PI);
+ Eigen::Vector3d axis(std::cos(theta) * std::sin(phi),
+ std::sin(theta) * std::sin(phi),
+ std::cos(phi));
+
+ // Construct a random angle
+ std::normal_distribution d{0, stddev};
+ double angle = d(gen);
+ q = Eigen::AngleAxisd(angle, axis);
+}
+
+void PrepareGravity(const colmap::Reconstruction& gt,
+ std::unordered_map& frames,
+ double gravity_noise_stddev = 0.0,
+ double outlier_ratio = 0.0) {
+ const Eigen::Vector3d kGravityInWorld = Eigen::Vector3d(0, 1, 0);
+ for (auto& frame_id : gt.RegFrameIds()) {
+ Eigen::Vector3d gravityInRig =
+ gt.Frame(frame_id).RigFromWorld().rotation * kGravityInWorld;
+
+ if (gravity_noise_stddev > 0.0) {
+ Eigen::Quaterniond noise;
+ CreateRandomRotation(DegToRad(gravity_noise_stddev), noise);
+ gravityInRig = noise * gravityInRig;
+ }
+
+ if (outlier_ratio > 0.0 &&
+ colmap::RandomUniformReal(0, 1) < outlier_ratio) {
+ Eigen::Quaterniond q;
+ CreateRandomRotation(1., q);
+ gravityInRig =
+ Rigid3dToAngleAxis(Rigid3d(q, Eigen::Vector3d::Zero())).normalized();
+ }
+
+ frames[frame_id].gravity_info.SetGravity(gravityInRig);
+ Rigid3d& cam_from_world = frames[frame_id].RigFromWorld();
+ cam_from_world.rotation = frames[frame_id].gravity_info.GetRAlign();
+ }
+}
+
+GlobalMapperOptions CreateMapperTestOptions() {
+ GlobalMapperOptions options;
+ options.skip_view_graph_calibration = false;
+ options.skip_relative_pose_estimation = false;
+ options.skip_rotation_averaging = true;
+ options.skip_track_establishment = true;
+ options.skip_global_positioning = true;
+ options.skip_bundle_adjustment = true;
+ options.skip_retriangulation = true;
+ return options;
+}
+
+RotationAveragerOptions CreateRATestOptions(bool use_gravity = false) {
+ RotationAveragerOptions options;
+ options.skip_initialization = false;
+ options.use_gravity = use_gravity;
+ options.use_stratified = true;
+ return options;
+}
+
+void ExpectEqualRotations(const colmap::Reconstruction& gt,
+ const colmap::Reconstruction& computed,
+ const double max_rotation_error_deg) {
+ const std::vector reg_image_ids = gt.RegImageIds();
+ for (size_t i = 0; i < reg_image_ids.size(); i++) {
+ const image_t image_id1 = reg_image_ids[i];
+ for (size_t j = 0; j < i; j++) {
+ const image_t image_id2 = reg_image_ids[j];
+
+ const Rigid3d cam2_from_cam1 =
+ computed.Image(image_id2).CamFromWorld() *
+ Inverse(computed.Image(image_id1).CamFromWorld());
+ const Rigid3d cam2_from_cam1_gt =
+ gt.Image(image_id2).CamFromWorld() *
+ Inverse(gt.Image(image_id1).CamFromWorld());
+
+ const double rotation_error_deg =
+ CalcAngle(cam2_from_cam1_gt, cam2_from_cam1);
+ EXPECT_LT(rotation_error_deg, max_rotation_error_deg);
+ }
+ }
+}
+
+void ExpectEqualGravity(
+ const colmap::Reconstruction& gt,
+ const std::unordered_map& images_computed,
+ const double max_gravity_error_deg) {
+ for (const auto& image_id : gt.RegImageIds()) {
+ if (!images_computed.at(image_id).HasTrivialFrame()) {
+ continue; // Skip images that are not trivial frames
+ }
+ const Eigen::Vector3d gravity_gt =
+ gt.Image(image_id).CamFromWorld().rotation * Eigen::Vector3d(0, 1, 0);
+ const Eigen::Vector3d gravity_computed =
+ images_computed.at(image_id).frame_ptr->gravity_info.GetGravity();
+
+ double gravity_error_deg = CalcAngle(gravity_gt, gravity_computed);
+ EXPECT_LT(gravity_error_deg, max_gravity_error_deg);
+ }
+}
+
+TEST(RotationEstimator, WithoutNoise) {
+ colmap::SetPRNGSeed(1);
+
+ const std::string database_path = colmap::CreateTestDir() + "/database.db";
+
+ auto database = colmap::Database::Open(database_path);
+ colmap::Reconstruction gt_reconstruction;
+ colmap::SyntheticDatasetOptions synthetic_dataset_options;
+ synthetic_dataset_options.num_rigs = 1;
+ synthetic_dataset_options.num_cameras_per_rig = 1;
+ synthetic_dataset_options.num_frames_per_rig = 5;
+ synthetic_dataset_options.num_points3D = 50;
+ synthetic_dataset_options.sensor_from_rig_rotation_stddev = 20.;
+ colmap::SynthesizeDataset(
+ synthetic_dataset_options, >_reconstruction, database.get());
+
+ ViewGraph view_graph;
+ std::unordered_map rigs;
+ std::unordered_map cameras;
+ std::unordered_map frames;
+ std::unordered_map images;
+ std::unordered_map tracks;
+
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
+
+ PrepareGravity(gt_reconstruction, frames);
+
+ GlobalMapper global_mapper(CreateMapperTestOptions());
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
+
+ // TODO: The current 1-dof rotation averaging sometimes fails to pick the
+ // right solution (e.g., 180 deg flipped).
+ for (const bool use_gravity : {false}) {
+ SolveRotationAveraging(
+ view_graph, rigs, frames, images, CreateRATestOptions(use_gravity));
+
+ colmap::Reconstruction reconstruction;
+ ConvertGlomapToColmap(
+ rigs, cameras, frames, images, tracks, reconstruction);
+ ExpectEqualRotations(
+ gt_reconstruction, reconstruction, /*max_rotation_error_deg=*/1e-2);
+ }
+}
+
+TEST(RotationEstimator, WithoutNoiseWithNoneTrivialKnownRig) {
+ colmap::SetPRNGSeed(1);
+
+ const std::string database_path = colmap::CreateTestDir() + "/database.db";
+
+ auto database = colmap::Database::Open(database_path);
+ colmap::Reconstruction gt_reconstruction;
+ colmap::SyntheticDatasetOptions synthetic_dataset_options;
+ synthetic_dataset_options.num_rigs = 1;
+ synthetic_dataset_options.num_cameras_per_rig = 2;
+ synthetic_dataset_options.num_frames_per_rig = 4;
+ synthetic_dataset_options.num_points3D = 50;
+ synthetic_dataset_options.sensor_from_rig_rotation_stddev = 20.;
+ colmap::SynthesizeDataset(
+ synthetic_dataset_options, >_reconstruction, database.get());
+
+ ViewGraph view_graph;
+ std::unordered_map rigs;
+ std::unordered_map cameras;
+ std::unordered_map frames;
+ std::unordered_map images;
+ std::unordered_map tracks;
+
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
+
+ PrepareGravity(gt_reconstruction, frames);
+
+ GlobalMapper global_mapper(CreateMapperTestOptions());
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
+
+ for (const bool use_gravity : {true, false}) {
+ SolveRotationAveraging(
+ view_graph, rigs, frames, images, CreateRATestOptions(use_gravity));
+
+ colmap::Reconstruction reconstruction;
+ ConvertGlomapToColmap(
+ rigs, cameras, frames, images, tracks, reconstruction);
+ ExpectEqualRotations(
+ gt_reconstruction, reconstruction, /*max_rotation_error_deg=*/1e-2);
+ }
+}
+
+TEST(RotationEstimator, WithoutNoiseWithNoneTrivialUnknownRig) {
+ colmap::SetPRNGSeed(1);
+
+ const std::string database_path = colmap::CreateTestDir() + "/database.db";
+
+ auto database = colmap::Database::Open(database_path);
+ colmap::Reconstruction gt_reconstruction;
+ colmap::SyntheticDatasetOptions synthetic_dataset_options;
+ synthetic_dataset_options.num_rigs = 1;
+ synthetic_dataset_options.num_cameras_per_rig = 2;
+ synthetic_dataset_options.num_frames_per_rig = 4;
+ synthetic_dataset_options.num_points3D = 50;
+ synthetic_dataset_options.sensor_from_rig_rotation_stddev = 20.;
+ colmap::SynthesizeDataset(
+ synthetic_dataset_options, >_reconstruction, database.get());
+
+ ViewGraph view_graph;
+ std::unordered_map rigs;
+ std::unordered_map cameras;
+ std::unordered_map frames;
+ std::unordered_map images;
+ std::unordered_map tracks;
+
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
+
+ for (auto& [rig_id, rig] : rigs) {
+ for (auto& [sensor_id, sensor] : rig.NonRefSensors()) {
+ if (sensor.has_value()) {
+ rig.ResetSensorFromRig(sensor_id);
+ }
+ }
+ }
+ PrepareGravity(gt_reconstruction, frames);
+
+ GlobalMapper global_mapper(CreateMapperTestOptions());
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
+
+ // For unknown rigs, it is not supported to use gravity.
+ for (const bool use_gravity : {false}) {
+ SolveRotationAveraging(
+ view_graph, rigs, frames, images, CreateRATestOptions(use_gravity));
+
+ colmap::Reconstruction reconstruction;
+ ConvertGlomapToColmap(
+ rigs, cameras, frames, images, tracks, reconstruction);
+ ExpectEqualRotations(
+ gt_reconstruction, reconstruction, /*max_rotation_error_deg=*/1e-2);
+ }
+}
+
+TEST(RotationEstimator, WithNoiseAndOutliers) {
+ colmap::SetPRNGSeed(1);
+
+ const std::string database_path = colmap::CreateTestDir() + "/database.db";
+
+ auto database = colmap::Database::Open(database_path);
+ colmap::Reconstruction gt_reconstruction;
+ colmap::SyntheticDatasetOptions synthetic_dataset_options;
+ synthetic_dataset_options.num_rigs = 2;
+ synthetic_dataset_options.num_cameras_per_rig = 1;
+ synthetic_dataset_options.num_frames_per_rig = 7;
+ synthetic_dataset_options.num_points3D = 100;
+ synthetic_dataset_options.inlier_match_ratio = 0.6;
+ colmap::SynthesizeDataset(
+ synthetic_dataset_options, >_reconstruction, database.get());
+ colmap::SyntheticNoiseOptions synthetic_noise_options;
+ synthetic_noise_options.point2D_stddev = 1;
+ colmap::SynthesizeNoise(
+ synthetic_noise_options, >_reconstruction, database.get());
+
+ ViewGraph view_graph;
+ std::unordered_map rigs;
+ std::unordered_map cameras;
+ std::unordered_map images;
+ std::unordered_map frames;
+ std::unordered_map tracks;
+
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
+
+ PrepareGravity(gt_reconstruction, frames, /*gravity_noise_stddev=*/3e-1);
+
+ GlobalMapper global_mapper(CreateMapperTestOptions());
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
+
+ // TODO: The current 1-dof rotation averaging sometimes fails to pick the
+ // right solution (e.g., 180 deg flipped).
+ for (const bool use_gravity : {false}) {
+ SolveRotationAveraging(
+ view_graph, rigs, frames, images, CreateRATestOptions(use_gravity));
+
+ colmap::Reconstruction reconstruction;
+ ConvertGlomapToColmap(
+ rigs, cameras, frames, images, tracks, reconstruction);
+ ExpectEqualRotations(
+ gt_reconstruction, reconstruction, /*max_rotation_error_deg=*/3);
+ }
+}
+
+TEST(RotationEstimator, WithNoiseAndOutliersWithNonTrivialKnownRigs) {
+ colmap::SetPRNGSeed(1);
+
+ const std::string database_path = colmap::CreateTestDir() + "/database.db";
+
+ auto database = colmap::Database::Open(database_path);
+ colmap::Reconstruction gt_reconstruction;
+ colmap::SyntheticDatasetOptions synthetic_dataset_options;
+ synthetic_dataset_options.num_rigs = 2;
+ synthetic_dataset_options.num_cameras_per_rig = 2;
+ synthetic_dataset_options.num_frames_per_rig = 7;
+ synthetic_dataset_options.num_points3D = 100;
+ synthetic_dataset_options.inlier_match_ratio = 0.6;
+ colmap::SynthesizeDataset(
+ synthetic_dataset_options, >_reconstruction, database.get());
+ colmap::SyntheticNoiseOptions synthetic_noise_options;
+ synthetic_noise_options.point2D_stddev = 1;
+ colmap::SynthesizeNoise(
+ synthetic_noise_options, >_reconstruction, database.get());
+
+ ViewGraph view_graph;
+ std::unordered_map rigs;
+ std::unordered_map cameras;
+ std::unordered_map images;
+ std::unordered_map frames;
+ std::unordered_map tracks;
+
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
+ PrepareGravity(gt_reconstruction, frames, /*gravity_noise_stddev=*/3e-1);
+
+ GlobalMapper global_mapper(CreateMapperTestOptions());
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
+
+ // TODO: The current 1-dof rotation averaging sometimes fails to pick the
+ // right solution (e.g., 180 deg flipped).
+ for (const bool use_gravity : {true, false}) {
+ SolveRotationAveraging(
+ view_graph, rigs, frames, images, CreateRATestOptions(use_gravity));
+
+ colmap::Reconstruction reconstruction;
+ ConvertGlomapToColmap(
+ rigs, cameras, frames, images, tracks, reconstruction);
+ if (use_gravity)
+ ExpectEqualRotations(
+ gt_reconstruction, reconstruction, /*max_rotation_error_deg=*/1.5);
+ else
+ ExpectEqualRotations(
+ gt_reconstruction, reconstruction, /*max_rotation_error_deg=*/2.);
+ }
+}
+
+TEST(RotationEstimator, RefineGravity) {
+ colmap::SetPRNGSeed(1);
+
+ const std::string database_path = colmap::CreateTestDir() + "/database.db";
+
+ auto database = colmap::Database::Open(database_path);
+ colmap::Reconstruction gt_reconstruction;
+ colmap::SyntheticDatasetOptions synthetic_dataset_options;
+ synthetic_dataset_options.num_rigs = 2;
+ synthetic_dataset_options.num_cameras_per_rig = 1;
+ synthetic_dataset_options.num_frames_per_rig = 25;
+ synthetic_dataset_options.num_points3D = 100;
+ colmap::SynthesizeDataset(
+ synthetic_dataset_options, >_reconstruction, database.get());
+
+ ViewGraph view_graph;
+ std::unordered_map rigs;
+ std::unordered_map cameras;
+ std::unordered_map frames;
+ std::unordered_map images;
+ std::unordered_map tracks;
+
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
+
+ PrepareGravity(gt_reconstruction,
+ frames,
+ /*gravity_noise_stddev=*/0.,
+ /*outlier_ratio=*/0.3);
+
+ GlobalMapper global_mapper(CreateMapperTestOptions());
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
+
+ GravityRefinerOptions opt_grav_refine;
+ GravityRefiner grav_refiner(opt_grav_refine);
+ grav_refiner.RefineGravity(view_graph, frames, images);
+
+ // Check whether the gravity does not have error after refinement
+ ExpectEqualGravity(gt_reconstruction,
+ images,
+ /*max_gravity_error_deg=*/1e-2);
+}
+
+TEST(RotationEstimator, RefineGravityWithNontrivialRigs) {
+ colmap::SetPRNGSeed(1);
+
+ const std::string database_path = colmap::CreateTestDir() + "/database.db";
+
+ auto database = colmap::Database::Open(database_path);
+ colmap::Reconstruction gt_reconstruction;
+ colmap::SyntheticDatasetOptions synthetic_dataset_options;
+ synthetic_dataset_options.num_rigs = 2;
+ synthetic_dataset_options.num_cameras_per_rig = 2;
+ synthetic_dataset_options.num_frames_per_rig = 25;
+ synthetic_dataset_options.num_points3D = 100;
+ colmap::SynthesizeDataset(
+ synthetic_dataset_options, >_reconstruction, database.get());
+
+ ViewGraph view_graph;
+ std::unordered_map rigs;
+ std::unordered_map cameras;
+ std::unordered_map frames;
+ std::unordered_map images;
+ std::unordered_map tracks;
+
+ ConvertDatabaseToGlomap(*database, view_graph, rigs, cameras, frames, images);
+
+ PrepareGravity(gt_reconstruction,
+ frames,
+ /*gravity_noise_stddev=*/0.,
+ /*outlier_ratio=*/0.3);
+
+ GlobalMapper global_mapper(CreateMapperTestOptions());
+ global_mapper.Solve(
+ *database, view_graph, rigs, cameras, frames, images, tracks);
+
+ GravityRefinerOptions opt_grav_refine;
+ GravityRefiner grav_refiner(opt_grav_refine);
+ grav_refiner.RefineGravity(view_graph, frames, images);
+
+ // Check whether the gravity does not have error after refinement
+ ExpectEqualGravity(gt_reconstruction,
+ images,
+ /*max_gravity_error_deg=*/1e-2);
+}
+
+} // namespace
+} // namespace glomap
diff --git a/glomap/controllers/track_establishment.cc b/glomap/controllers/track_establishment.cc
index 105f7763..77c307ec 100644
--- a/glomap/controllers/track_establishment.cc
+++ b/glomap/controllers/track_establishment.cc
@@ -5,7 +5,7 @@ namespace glomap {
size_t TrackEngine::EstablishFullTracks(
std::unordered_map& tracks) {
tracks.clear();
- uf_.Clear();
+ uf_ = {};
// Blindly concatenate tracks if any matches occur
BlindConcatenation();
@@ -173,7 +173,7 @@ size_t TrackEngine::FindTracksForProblem(
// corresponding to those images
std::unordered_map tracks;
for (const auto& [image_id, image] : images_) {
- if (!image.is_registered) continue;
+ if (!image.IsRegistered()) continue;
tracks_per_camera[image_id] = 0;
}
diff --git a/glomap/controllers/track_establishment.h b/glomap/controllers/track_establishment.h
index 7e0c3ef0..1eb6a58b 100644
--- a/glomap/controllers/track_establishment.h
+++ b/glomap/controllers/track_establishment.h
@@ -1,9 +1,10 @@
#pragma once
-#include "glomap/math/union_find.h"
#include "glomap/scene/types_sfm.h"
+#include
+
namespace glomap {
struct TrackEstablishmentOptions {
@@ -53,7 +54,7 @@ class TrackEngine {
const std::unordered_map& images_;
// Internal structure used for concatenating tracks
- UnionFind uf_;
+ colmap::UnionFind uf_;
};
} // namespace glomap
diff --git a/glomap/controllers/track_retriangulation.cc b/glomap/controllers/track_retriangulation.cc
index 18559c20..0b133e0d 100644
--- a/glomap/controllers/track_retriangulation.cc
+++ b/glomap/controllers/track_retriangulation.cc
@@ -2,15 +2,19 @@
#include "glomap/io/colmap_converter.h"
-#include
+#include
#include
#include
+#include
+
namespace glomap {
bool RetriangulateTracks(const TriangulatorOptions& options,
const colmap::Database& database,
+ std::unordered_map& rigs,
std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map& tracks) {
// Following code adapted from COLMAP
@@ -26,21 +30,23 @@ bool RetriangulateTracks(const TriangulatorOptions& options,
std::vector image_ids_notconnected;
for (auto& image : images) {
if (!database_cache->ExistsImage(image.first) &&
- image.second.is_registered) {
- image.second.is_registered = false;
+ image.second.IsRegistered()) {
image_ids_notconnected.push_back(image.first);
+ image.second.frame_ptr->is_registered = false;
}
}
// Convert the glomap data structures to colmap data structures
std::shared_ptr reconstruction_ptr =
std::make_shared();
- ConvertGlomapToColmap(cameras,
+ ConvertGlomapToColmap(rigs,
+ cameras,
+ frames,
images,
std::unordered_map(),
*reconstruction_ptr);
- colmap::IncrementalMapperOptions options_colmap;
+ colmap::IncrementalPipelineOptions options_colmap;
options_colmap.triangulation.complete_max_reproj_error =
options.tri_complete_max_reproj_error;
options_colmap.triangulation.merge_max_reproj_error =
@@ -57,13 +63,13 @@ bool RetriangulateTracks(const TriangulatorOptions& options,
const auto tri_options = options_colmap.Triangulation();
const auto mapper_options = options_colmap.Mapper();
- const std::vector& reg_image_ids = reconstruction_ptr->RegImageIds();
+ const std::vector reg_image_ids = reconstruction_ptr->RegImageIds();
- for (size_t i = 0; i < reg_image_ids.size(); ++i) {
- std::cout << "\r Triangulating image " << i + 1 << " / "
+ size_t image_idx = 0;
+ for (const image_t image_id : reg_image_ids) {
+ std::cout << "\r Triangulating image " << image_idx++ + 1 << " / "
<< reg_image_ids.size() << std::flush;
- const image_t image_id = reg_image_ids[i];
const auto& image = reconstruction_ptr->Image(image_id);
int num_tris = mapper.TriangulateImage(tri_options, image_id);
@@ -77,7 +83,8 @@ bool RetriangulateTracks(const TriangulatorOptions& options,
ba_options.refine_focal_length = false;
ba_options.refine_principal_point = false;
ba_options.refine_extra_params = false;
- ba_options.refine_extrinsics = false;
+ ba_options.refine_sensor_from_rig = false;
+ ba_options.refine_rig_from_world = false;
// Configure bundle adjustment.
colmap::BundleAdjustmentConfig ba_config;
@@ -96,10 +103,10 @@ bool RetriangulateTracks(const TriangulatorOptions& options,
const size_t num_observations =
reconstruction_ptr->ComputeNumObservations();
- // PrintHeading1("Bundle adjustment");
- colmap::BundleAdjuster bundle_adjuster(ba_options, ba_config);
- // THROW_CHECK(bundle_adjuster.Solve(reconstruction.get()));
- if (!bundle_adjuster.Solve(reconstruction_ptr.get())) {
+ std::unique_ptr bundle_adjuster;
+ bundle_adjuster =
+ CreateDefaultBundleAdjuster(ba_options, ba_config, *reconstruction_ptr);
+ if (bundle_adjuster->Solve().termination_type == ceres::FAILURE) {
return false;
}
@@ -116,16 +123,17 @@ bool RetriangulateTracks(const TriangulatorOptions& options,
// Add the removed images to the reconstruction
for (const auto& image_id : image_ids_notconnected) {
- images[image_id].is_registered = true;
+ images[image_id].frame_ptr->is_registered = true;
colmap::Image image_colmap;
ConvertGlomapToColmapImage(images[image_id], image_colmap, true);
reconstruction_ptr->AddImage(std::move(image_colmap));
}
// Convert the colmap data structures back to glomap data structures
- ConvertColmapToGlomap(*reconstruction_ptr, cameras, images, tracks);
+ ConvertColmapToGlomap(
+ *reconstruction_ptr, rigs, cameras, frames, images, tracks);
return true;
}
-} // namespace glomap
\ No newline at end of file
+} // namespace glomap
diff --git a/glomap/controllers/track_retriangulation.h b/glomap/controllers/track_retriangulation.h
index 6b058515..169eae79 100644
--- a/glomap/controllers/track_retriangulation.h
+++ b/glomap/controllers/track_retriangulation.h
@@ -17,7 +17,9 @@ struct TriangulatorOptions {
bool RetriangulateTracks(const TriangulatorOptions& options,
const colmap::Database& database,
+ std::unordered_map& rigs,
std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map& tracks);
diff --git a/glomap/estimators/bundle_adjustment.cc b/glomap/estimators/bundle_adjustment.cc
index a03bea5c..8edffb01 100644
--- a/glomap/estimators/bundle_adjustment.cc
+++ b/glomap/estimators/bundle_adjustment.cc
@@ -3,11 +3,14 @@
#include
#include
#include
+#include
+#include
namespace glomap {
-bool BundleAdjuster::Solve(const ViewGraph& view_graph,
+bool BundleAdjuster::Solve(std::unordered_map& rigs,
std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map& tracks) {
// Check if the input data is valid
@@ -24,25 +27,77 @@ bool BundleAdjuster::Solve(const ViewGraph& view_graph,
Reset();
// Add the constraints that the point tracks impose on the problem
- AddPointToCameraConstraints(view_graph, cameras, images, tracks);
+ AddPointToCameraConstraints(rigs, cameras, frames, images, tracks);
// Add the cameras and points to the parameter groups for schur-based
// optimization
- AddCamerasAndPointsToParameterGroups(cameras, images, tracks);
+ AddCamerasAndPointsToParameterGroups(rigs, cameras, frames, tracks);
// Parameterize the variables
- ParameterizeVariables(cameras, images, tracks);
+ ParameterizeVariables(rigs, cameras, frames, tracks);
// Set the solver options.
ceres::Solver::Summary summary;
+ int num_images = images.size();
+#ifdef GLOMAP_CUDA_ENABLED
+ bool cuda_solver_enabled = false;
+
+#if (CERES_VERSION_MAJOR >= 3 || \
+ (CERES_VERSION_MAJOR == 2 && CERES_VERSION_MINOR >= 2)) && \
+ !defined(CERES_NO_CUDA)
+ if (options_.use_gpu && num_images >= options_.min_num_images_gpu_solver) {
+ cuda_solver_enabled = true;
+ options_.solver_options.dense_linear_algebra_library_type = ceres::CUDA;
+ }
+#else
+ if (options_.use_gpu) {
+ LOG_FIRST_N(WARNING, 1)
+ << "Requested to use GPU for bundle adjustment, but Ceres was "
+ "compiled without CUDA support. Falling back to CPU-based dense "
+ "solvers.";
+ }
+#endif
+
+#if (CERES_VERSION_MAJOR >= 3 || \
+ (CERES_VERSION_MAJOR == 2 && CERES_VERSION_MINOR >= 3)) && \
+ !defined(CERES_NO_CUDSS)
+ if (options_.use_gpu && num_images >= options_.min_num_images_gpu_solver) {
+ cuda_solver_enabled = true;
+ options_.solver_options.sparse_linear_algebra_library_type =
+ ceres::CUDA_SPARSE;
+ }
+#else
+ if (options_.use_gpu) {
+ LOG_FIRST_N(WARNING, 1)
+ << "Requested to use GPU for bundle adjustment, but Ceres was "
+ "compiled without cuDSS support. Falling back to CPU-based sparse "
+ "solvers.";
+ }
+#endif
+
+ if (cuda_solver_enabled) {
+ const std::vector gpu_indices =
+ colmap::CSVToVector(options_.gpu_index);
+ THROW_CHECK_GT(gpu_indices.size(), 0);
+ colmap::SetBestCudaDevice(gpu_indices[0]);
+ }
+#else
+ if (options_.use_gpu) {
+ LOG_FIRST_N(WARNING, 1)
+ << "Requested to use GPU for bundle adjustment, but COLMAP was "
+ "compiled without CUDA support. Falling back to CPU-based "
+ "solvers.";
+ }
+#endif // GLOMAP_CUDA_ENABLED
+
// Do not use the iterative solver, as it does not seem to be helpful
options_.solver_options.linear_solver_type = ceres::SPARSE_SCHUR;
options_.solver_options.preconditioner_type = ceres::CLUSTER_TRIDIAGONAL;
- options_.solver_options.minimizer_progress_to_stdout = options_.verbose;
+ options_.solver_options.minimizer_progress_to_stdout = VLOG_IS_ON(2);
ceres::Solve(options_.solver_options, problem_.get(), &summary);
- if (options_.verbose)
+ if (VLOG_IS_ON(2))
LOG(INFO) << summary.FullReport();
else
LOG(INFO) << summary.BriefReport();
@@ -54,11 +109,13 @@ void BundleAdjuster::Reset() {
ceres::Problem::Options problem_options;
problem_options.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP;
problem_ = std::make_unique(problem_options);
+ loss_function_ = options_.CreateLossFunction();
}
void BundleAdjuster::AddPointToCameraConstraints(
- const ViewGraph& view_graph,
+ std::unordered_map& rigs,
std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map& tracks) {
for (auto& [track_id, track] : tracks) {
@@ -68,20 +125,61 @@ void BundleAdjuster::AddPointToCameraConstraints(
if (images.find(observation.first) == images.end()) continue;
Image& image = images[observation.first];
+ Frame* frame_ptr = image.frame_ptr;
+ camera_t camera_id = image.camera_id;
+ image_t rig_id = image.frame_ptr->RigId();
- ceres::CostFunction* cost_function =
- colmap::CameraCostFunction(
- cameras[image.camera_id].model_id,
- image.features[observation.second]);
-
- if (cost_function != nullptr) {
+ ceres::CostFunction* cost_function = nullptr;
+ // if (image_id_to_camera_rig_index_.find(observation.first) ==
+ // image_id_to_camera_rig_index_.end()) {
+ if (image.HasTrivialFrame()) {
+ cost_function =
+ colmap::CreateCameraCostFunction(
+ cameras[image.camera_id].model_id,
+ image.features[observation.second]);
+ problem_->AddResidualBlock(
+ cost_function,
+ loss_function_.get(),
+ frame_ptr->RigFromWorld().rotation.coeffs().data(),
+ frame_ptr->RigFromWorld().translation.data(),
+ tracks[track_id].xyz.data(),
+ cameras[image.camera_id].params.data());
+ } else if (!options_.optimize_rig_poses) {
+ const Rigid3d& cam_from_rig = rigs[rig_id].SensorFromRig(
+ sensor_t(SensorType::CAMERA, image.camera_id));
+ cost_function = colmap::CreateCameraCostFunction<
+ colmap::RigReprojErrorConstantRigCostFunctor>(
+ cameras[image.camera_id].model_id,
+ image.features[observation.second],
+ cam_from_rig);
+ problem_->AddResidualBlock(
+ cost_function,
+ loss_function_.get(),
+ frame_ptr->RigFromWorld().rotation.coeffs().data(),
+ frame_ptr->RigFromWorld().translation.data(),
+ tracks[track_id].xyz.data(),
+ cameras[image.camera_id].params.data());
+ } else {
+ // If the image is part of a camera rig, use the RigBATA error
+ // Down weight the uncalibrated cameras
+ Rigid3d& cam_from_rig = rigs[rig_id].SensorFromRig(
+ sensor_t(SensorType::CAMERA, image.camera_id));
+ cost_function =
+ colmap::CreateCameraCostFunction(
+ cameras[image.camera_id].model_id,
+ image.features[observation.second]);
problem_->AddResidualBlock(
cost_function,
- options_.loss_function.get(),
- image.cam_from_world.rotation.coeffs().data(),
- image.cam_from_world.translation.data(),
+ loss_function_.get(),
+ cam_from_rig.rotation.coeffs().data(),
+ cam_from_rig.translation.data(),
+ frame_ptr->RigFromWorld().rotation.coeffs().data(),
+ frame_ptr->RigFromWorld().translation.data(),
tracks[track_id].xyz.data(),
cameras[image.camera_id].params.data());
+ }
+
+ if (cost_function != nullptr) {
} else {
LOG(ERROR) << "Camera model not supported: "
<< colmap::CameraModelIdToName(
@@ -92,8 +190,9 @@ void BundleAdjuster::AddPointToCameraConstraints(
}
void BundleAdjuster::AddCamerasAndPointsToParameterGroups(
+ std::unordered_map& rigs,
std::unordered_map& cameras,
- std::unordered_map& images,
+ std::unordered_map& frames,
std::unordered_map& tracks) {
if (tracks.size() == 0) return;
@@ -108,13 +207,30 @@ void BundleAdjuster::AddCamerasAndPointsToParameterGroups(
parameter_ordering->AddElementToGroup(track.xyz.data(), 0);
}
- // Add camera parameters to group 1.
- for (auto& [image_id, image] : images) {
- if (problem_->HasParameterBlock(image.cam_from_world.translation.data())) {
+ // Add frame parameters to group 1.
+ for (auto& [frame_id, frame] : frames) {
+ if (!frame.HasPose()) continue;
+ if (problem_->HasParameterBlock(frame.RigFromWorld().translation.data())) {
parameter_ordering->AddElementToGroup(
- image.cam_from_world.translation.data(), 1);
+ frame.RigFromWorld().translation.data(), 1);
parameter_ordering->AddElementToGroup(
- image.cam_from_world.rotation.coeffs().data(), 1);
+ frame.RigFromWorld().rotation.coeffs().data(), 1);
+ }
+ }
+
+ // Add the cam_from_rigs to be estimated into the parameter group
+ for (auto& [rig_id, rig] : rigs) {
+ for (const auto& [sensor_id, sensor] : rig.NonRefSensors()) {
+ if (sensor_id.type == SensorType::CAMERA) {
+ Eigen::Vector3d& translation = rig.SensorFromRig(sensor_id).translation;
+ if (problem_->HasParameterBlock(translation.data())) {
+ parameter_ordering->AddElementToGroup(translation.data(), 1);
+ }
+ Eigen::Quaterniond& rotation = rig.SensorFromRig(sensor_id).rotation;
+ if (problem_->HasParameterBlock(rotation.coeffs().data())) {
+ parameter_ordering->AddElementToGroup(rotation.coeffs().data(), 1);
+ }
+ }
}
}
@@ -126,41 +242,35 @@ void BundleAdjuster::AddCamerasAndPointsToParameterGroups(
}
void BundleAdjuster::ParameterizeVariables(
+ std::unordered_map& rigs,
std::unordered_map& cameras,
- std::unordered_map& images,
+ std::unordered_map& frames,
std::unordered_map& tracks) {
- image_t center;
+ frame_t center;
// Parameterize rotations, and set rotations and translations to be constant
// if desired FUTURE: Consider fix the scale of the reconstruction
int counter = 0;
- for (auto& [image_id, image] : images) {
+ for (auto& [frame_id, frame] : frames) {
+ if (!frame.HasPose()) continue;
if (problem_->HasParameterBlock(
- image.cam_from_world.rotation.coeffs().data())) {
+ frame.RigFromWorld().rotation.coeffs().data())) {
colmap::SetQuaternionManifold(
- problem_.get(), image.cam_from_world.rotation.coeffs().data());
+ problem_.get(), frame.RigFromWorld().rotation.coeffs().data());
- if (counter == 0) {
- center = image_id;
- counter++;
- }
- if (!options_.optimize_rotations)
+ if (!options_.optimize_rotations || counter == 0)
problem_->SetParameterBlockConstant(
- image.cam_from_world.rotation.coeffs().data());
- if (!options_.optimize_translation)
+ frame.RigFromWorld().rotation.coeffs().data());
+ if (!options_.optimize_translation || counter == 0)
problem_->SetParameterBlockConstant(
- image.cam_from_world.translation.data());
+ frame.RigFromWorld().translation.data());
+
+ counter++;
}
}
- // Set the first camera to be fixed to remove the gauge ambiguity.
- problem_->SetParameterBlockConstant(
- images[center].cam_from_world.rotation.coeffs().data());
- problem_->SetParameterBlockConstant(
- images[center].cam_from_world.translation.data());
-
// Parameterize the camera parameters, or set them to be constant if desired
- if (options_.optimize_intrinsics) {
+ if (options_.optimize_intrinsics && !options_.optimize_principal_point) {
for (auto& [camera_id, camera] : cameras) {
if (problem_->HasParameterBlock(camera.params.data())) {
std::vector principal_point_idxs;
@@ -173,8 +283,8 @@ void BundleAdjuster::ParameterizeVariables(
camera.params.data());
}
}
-
- } else {
+ } else if (!options_.optimize_intrinsics &&
+ !options_.optimize_principal_point) {
for (auto& [camera_id, camera] : cameras) {
if (problem_->HasParameterBlock(camera.params.data())) {
problem_->SetParameterBlockConstant(camera.params.data());
@@ -182,6 +292,21 @@ void BundleAdjuster::ParameterizeVariables(
}
}
+ // If we optimize the rig poses, then parameterize them
+ if (options_.optimize_rig_poses) {
+ for (auto& [rig_id, rig] : rigs) {
+ for (const auto& [sensor_id, sensor] : rig.NonRefSensors()) {
+ if (sensor_id.type == SensorType::CAMERA) {
+ Eigen::Quaterniond& rotation = rig.SensorFromRig(sensor_id).rotation;
+ if (problem_->HasParameterBlock(rotation.coeffs().data())) {
+ colmap::SetQuaternionManifold(problem_.get(),
+ rotation.coeffs().data());
+ }
+ }
+ }
+ }
+ }
+
if (!options_.optimize_points) {
for (auto& [track_id, track] : tracks) {
if (problem_->HasParameterBlock(track.xyz.data())) {
diff --git a/glomap/estimators/bundle_adjustment.h b/glomap/estimators/bundle_adjustment.h
index 97419a5d..5fdd92f6 100644
--- a/glomap/estimators/bundle_adjustment.h
+++ b/glomap/estimators/bundle_adjustment.h
@@ -1,5 +1,6 @@
#pragma once
+// #include "glomap/estimators/bundle_adjustment.h"
#include "glomap/estimators/optimization_base.h"
#include "glomap/scene/types_sfm.h"
#include "glomap/types.h"
@@ -11,21 +12,29 @@ namespace glomap {
struct BundleAdjusterOptions : public OptimizationBaseOptions {
public:
// Flags for which parameters to optimize
+ bool optimize_rig_poses = false; // Whether to optimize the rig poses
bool optimize_rotations = true;
bool optimize_translation = true;
bool optimize_intrinsics = true;
+ bool optimize_principal_point = false;
bool optimize_points = true;
+ bool use_gpu = true;
+ std::string gpu_index = "-1";
+ int min_num_images_gpu_solver = 50;
+
// Constrain the minimum number of views per track
int min_num_view_per_track = 3;
BundleAdjusterOptions() : OptimizationBaseOptions() {
thres_loss_function = 1.;
- loss_function = std::make_shared(thres_loss_function);
solver_options.max_num_iterations = 200;
}
-};
+ std::shared_ptr CreateLossFunction() {
+ return std::make_shared(thres_loss_function);
+ }
+};
class BundleAdjuster {
public:
BundleAdjuster(const BundleAdjusterOptions& options) : options_(options) {}
@@ -33,8 +42,9 @@ class BundleAdjuster {
// Returns true if the optimization was a success, false if there was a
// failure.
// Assume tracks here are already filtered
- bool Solve(const ViewGraph& view_graph,
+ bool Solve(std::unordered_map& rigs,
std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map& tracks);
@@ -46,25 +56,29 @@ class BundleAdjuster {
// Add tracks to the problem
void AddPointToCameraConstraints(
- const ViewGraph& view_graph,
+ std::unordered_map& rigs,
std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map& tracks);
// Set the parameter groups
void AddCamerasAndPointsToParameterGroups(
+ std::unordered_map& rigs,
std::unordered_map& cameras,
- std::unordered_map& images,
+ std::unordered_map& frames,
std::unordered_map& tracks);
// Parameterize the variables, set some variables to be constant if desired
- void ParameterizeVariables(std::unordered_map& cameras,
- std::unordered_map& images,
+ void ParameterizeVariables(std::unordered_map& rigs,
+ std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& tracks);
BundleAdjusterOptions options_;
std::unique_ptr problem_;
+ std::shared_ptr loss_function_;
};
} // namespace glomap
diff --git a/glomap/estimators/cost_function.h b/glomap/estimators/cost_function.h
index d0a8ce48..ed21efdf 100644
--- a/glomap/estimators/cost_function.h
+++ b/glomap/estimators/cost_function.h
@@ -14,7 +14,7 @@ namespace glomap {
// from two positions such that t_ij - scale * (c_j - c_i) is minimized.
struct BATAPairwiseDirectionError {
BATAPairwiseDirectionError(const Eigen::Vector3d& translation_obs)
- : translation_obs_(translation_obs){};
+ : translation_obs_(translation_obs) {}
// The error is given by the position error described above.
template
@@ -22,30 +22,117 @@ struct BATAPairwiseDirectionError {
const T* position2,
const T* scale,
T* residuals) const {
- Eigen::Matrix translation;
- translation[0] = position2[0] - position1[0];
- translation[1] = position2[1] - position1[1];
- translation[2] = position2[2] - position1[2];
+ Eigen::Map> residuals_vec(residuals);
+ residuals_vec =
+ translation_obs_.cast() -
+ scale[0] * (Eigen::Map>(position2) -
+ Eigen::Map>(position1));
+ return true;
+ }
- Eigen::Matrix residual_vec;
+ static ceres::CostFunction* Create(const Eigen::Vector3d& translation_obs) {
+ return (
+ new ceres::AutoDiffCostFunction(
+ new BATAPairwiseDirectionError(translation_obs)));
+ }
- residual_vec = translation_obs_.cast() - scale[0] * translation;
+ // TODO: add covariance
+ const Eigen::Vector3d translation_obs_;
+};
- residuals[0] = residual_vec(0);
- residuals[1] = residual_vec(1);
- residuals[2] = residual_vec(2);
+// ----------------------------------------
+// RigBATAPairwiseDirectionError
+// ----------------------------------------
+// Computes the error between a translation direction and the direction formed
+// from two positions such that t_ij - scale * (c_j - c_i + scale_rig * t_rig)
+// is minimized.
+struct RigBATAPairwiseDirectionError {
+ RigBATAPairwiseDirectionError(const Eigen::Vector3d& translation_obs,
+ const Eigen::Vector3d& translation_rig)
+ : translation_obs_(translation_obs), translation_rig_(translation_rig) {}
+ // The error is given by the position error described above.
+ template
+ bool operator()(const T* position1,
+ const T* position2,
+ const T* scale,
+ const T* scale_rig,
+ T* residuals) const {
+ Eigen::Map> residuals_vec(residuals);
+ residuals_vec =
+ translation_obs_.cast() -
+ scale[0] * (Eigen::Map>(position2) -
+ Eigen::Map>(position1) +
+ scale_rig[0] * translation_rig_.cast());
return true;
}
- static ceres::CostFunction* Create(const Eigen::Vector3d& translation_obs) {
+ static ceres::CostFunction* Create(const Eigen::Vector3d& translation_obs,
+ const Eigen::Vector3d& translation_rig) {
return (
- new ceres::AutoDiffCostFunction(
- new BATAPairwiseDirectionError(translation_obs)));
+ new ceres::
+ AutoDiffCostFunction(
+ new RigBATAPairwiseDirectionError(translation_obs,
+ translation_rig)));
+ }
+
+ // TODO: add covariance
+ const Eigen::Vector3d translation_obs_;
+ const Eigen::Vector3d translation_rig_; // = c_R_w^T * c_t_r
+};
+
+// ----------------------------------------
+// RigUnknownBATAPairwiseDirectionError
+// ----------------------------------------
+// Computes the error between a translation direction and the direction formed
+// from three positions such that v - scale * ((X - r_c_w) - r_R_w^T * c_c_r) is
+// minimized.
+struct RigUnknownBATAPairwiseDirectionError {
+ RigUnknownBATAPairwiseDirectionError(
+ const Eigen::Vector3d& translation_obs,
+ const Eigen::Quaterniond& rig_from_world_rot)
+ : translation_obs_(translation_obs),
+ rig_from_world_rot_(rig_from_world_rot) {}
+
+ // The error is given by the position error described above.
+ template
+ bool operator()(const T* point3d,
+ const T* rig_from_world_center,
+ const T* cam_from_rig_center,
+ const T* scale,
+ T* residuals) const {
+ Eigen::Map> residuals_vec(residuals);
+
+ Eigen::Matrix translation_rig =
+ rig_from_world_rot_.toRotationMatrix().transpose() *
+ Eigen::Map>(cam_from_rig_center);
+
+ residuals_vec =
+ translation_obs_.cast() -
+ scale[0] *
+ (Eigen::Map>(point3d) -
+ Eigen::Map>(rig_from_world_center) -
+ translation_rig);
+ return true;
+ }
+
+ static ceres::CostFunction* Create(
+ const Eigen::Vector3d& translation_obs,
+ const Eigen::Quaterniond& rig_from_world_rot) {
+ return (
+ new ceres::AutoDiffCostFunction(
+ new RigUnknownBATAPairwiseDirectionError(translation_obs,
+ rig_from_world_rot)));
}
// TODO: add covariance
const Eigen::Vector3d translation_obs_;
+ const Eigen::Quaterniond rig_from_world_rot_; // = c_R_w^T * c_t_r
};
// ----------------------------------------
diff --git a/glomap/estimators/global_positioning.cc b/glomap/estimators/global_positioning.cc
index 9c566f6c..f5f96a99 100644
--- a/glomap/estimators/global_positioning.cc
+++ b/glomap/estimators/global_positioning.cc
@@ -2,9 +2,11 @@
#include "glomap/estimators/cost_function.h"
#include "glomap/io/recording.h"
-#include
#include "glomap/io/colmap_io.h"
+#include "glomap/math/rigid3d.h"
+#include
+#include
#include
namespace glomap {
@@ -24,41 +26,46 @@ Eigen::Vector3d RandVector3d(std::mt19937& random_generator,
class LoggingCallback : public ceres::IterationCallback {
-public:
- std::unordered_map& tracks;
+public:
+ std::unordered_map& rigs;
std::unordered_map& cameras;
+ std::unordered_map& frames;
std::unordered_map& images;
+ std::unordered_map& tracks;
std::string image_path;
- LoggingCallback(std::unordered_map& tracks,
+ LoggingCallback(std::unordered_map& rigs,
std::unordered_map& cameras,
- std::unordered_map& images,
+ std::unordered_map& frames,
+ std::unordered_map& images,
+ std::unordered_map& tracks,
std::string image_path
- ) : tracks {tracks}, cameras {cameras}, images {images}, image_path {image_path} {}
+ ) : rigs{rigs}, cameras{cameras}, frames{frames}, images{images}, tracks{tracks}, image_path{image_path} {}
~LoggingCallback() {}
ceres::CallbackReturnType operator()(const ceres::IterationSummary& summary) {
std::unordered_map images_copy = images;
for (auto& [image_id, image] : images_copy) {
- image.cam_from_world.translation =
- -(image.cam_from_world.rotation * image.cam_from_world.translation);
+ if (!image.IsRegistered()) continue;
+ image.CamFromWorld().translation =
+ -(image.CamFromWorld().rotation * image.CamFromWorld().translation);
}
rr_rec.set_time_sequence("step", algorithm_step++);
if (summary.iteration == 0) {
// This is a bit of a hack to extract the colors for the point cloud to make the visualization a bit prettier.
-
+ std::unordered_map tmp_rigs;
std::unordered_map tmp_cameras;
+ std::unordered_map tmp_frames;
std::unordered_map tmp_images;
std::unordered_map tmp_tracks;
colmap::Reconstruction reconstruction;
- // ConvertDatabaseToColmap(re)
- ConvertGlomapToColmap(cameras, images_copy, tracks, reconstruction);
+ ConvertGlomapToColmap(rigs, cameras, frames, images_copy, tracks, reconstruction);
reconstruction.ExtractColorsForAllImages(image_path);
- ConvertColmapToGlomap(reconstruction, tmp_cameras, tmp_images, tmp_tracks);
+ ConvertColmapToGlomap(reconstruction, tmp_rigs, tmp_cameras, tmp_frames, tmp_images, tmp_tracks);
for (auto &[track_id, track] : tmp_tracks) {
tracks[track_id].color = track.color;
}
@@ -79,9 +86,14 @@ GlobalPositioner::GlobalPositioner(const GlobalPositionerOptions& options)
}
bool GlobalPositioner::Solve(const ViewGraph& view_graph,
+ std::unordered_map& rigs,
std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map& tracks) {
+ if (rigs.size() > 1) {
+ LOG(ERROR) << "Number of camera rigs = " << rigs.size();
+ }
if (images.empty()) {
LOG(ERROR) << "Number of images = " << images.size();
return false;
@@ -99,141 +111,174 @@ bool GlobalPositioner::Solve(const ViewGraph& view_graph,
LOG(INFO) << "Setting up the global positioner problem";
- // Initialize the problem
- Reset();
+ // Setup the problem.
+ SetupProblem(view_graph, rigs, tracks);
// Initialize camera translations to be random.
// Also, convert the camera pose translation to be the camera center.
- InitializeRandomPositions(view_graph, images, tracks);
+ InitializeRandomPositions(view_graph, frames, images, tracks);
// Add the camera to camera constraints to the problem.
+ // TODO: support the relative constraints with trivial frames to a non trivial
+ // frame
if (options_.constraint_type != GlobalPositionerOptions::ONLY_POINTS) {
AddCameraToCameraConstraints(view_graph, images);
}
// Add the point to camera constraints to the problem.
if (options_.constraint_type != GlobalPositionerOptions::ONLY_CAMERAS) {
- AddPointToCameraConstraints(cameras, images, tracks);
+ AddPointToCameraConstraints(rigs, cameras, frames, images, tracks);
}
- AddCamerasAndPointsToParameterGroups(images, tracks);
+ AddCamerasAndPointsToParameterGroups(rigs, frames, tracks);
// Parameterize the variables, set image poses / tracks / scales to be
// constant if desired
- ParameterizeVariables(images, tracks);
+ ParameterizeVariables(rigs, frames, tracks);
LOG(INFO) << "Solving the global positioner problem";
- LoggingCallback callback {tracks, cameras, images, image_path_global};
+ LoggingCallback callback {rigs, cameras, frames, images, tracks, image_path_global};
ceres::Solver::Summary summary;
- options_.solver_options.minimizer_progress_to_stdout = options_.verbose;
+ options_.solver_options.minimizer_progress_to_stdout = VLOG_IS_ON(2);
auto solver_options = options_.solver_options;
solver_options.callbacks.push_back(&callback);
solver_options.update_state_every_iteration = true;
-
+
ceres::Solve(solver_options, problem_.get(), &summary);
- if (options_.verbose) {
+ if (VLOG_IS_ON(2)) {
LOG(INFO) << summary.FullReport();
} else {
LOG(INFO) << summary.BriefReport();
}
- ConvertResults(images);
+ ConvertResults(rigs, frames);
return summary.IsSolutionUsable();
}
-void GlobalPositioner::Reset() {
+void GlobalPositioner::SetupProblem(
+ const ViewGraph& view_graph,
+ const std::unordered_map& rigs,
+ const std::unordered_map& tracks) {
ceres::Problem::Options problem_options;
problem_options.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP;
problem_ = std::make_unique(problem_options);
+ loss_function_ = options_.CreateLossFunction();
+
+ // Allocate enough memory for the scales. One for each residual.
+ // Due to possibly invalid image pairs or tracks, the actual number of
+ // residuals may be smaller.
scales_.clear();
+ scales_.reserve(
+ view_graph.image_pairs.size() +
+ std::accumulate(tracks.begin(),
+ tracks.end(),
+ 0,
+ [](int sum, const std::pair& track) {
+ return sum + track.second.observations.size();
+ }));
+
+ // Initialize the rig scales to be 1.0.
+ for (const auto& [rig_id, rig] : rigs) {
+ rig_scales_.emplace(rig_id, 1.0);
+ }
}
void GlobalPositioner::InitializeRandomPositions(
const ViewGraph& view_graph,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map& tracks) {
std::unordered_set constrained_positions;
- constrained_positions.reserve(images.size());
+ constrained_positions.reserve(frames.size());
for (const auto& [pair_id, image_pair] : view_graph.image_pairs) {
if (image_pair.is_valid == false) continue;
-
- constrained_positions.insert(image_pair.image_id1);
- constrained_positions.insert(image_pair.image_id2);
+ constrained_positions.insert(images[image_pair.image_id1].frame_id);
+ constrained_positions.insert(images[image_pair.image_id2].frame_id);
}
- if (options_.constraint_type != GlobalPositionerOptions::ONLY_CAMERAS) {
- for (const auto& [track_id, track] : tracks) {
- if (track.observations.size() < options_.min_num_view_per_track) continue;
- for (const auto& observation : tracks[track_id].observations) {
- if (images.find(observation.first) == images.end()) continue;
- Image& image = images[observation.first];
- if (!image.is_registered) continue;
- constrained_positions.insert(observation.first);
- }
+ for (const auto& [track_id, track] : tracks) {
+ if (track.observations.size() < options_.min_num_view_per_track) continue;
+ for (const auto& observation : tracks[track_id].observations) {
+ if (images.find(observation.first) == images.end()) continue;
+ Image& image = images[observation.first];
+ if (!image.IsRegistered()) continue;
+ constrained_positions.insert(images[observation.first].frame_id);
}
}
if (!options_.generate_random_positions || !options_.optimize_positions) {
- for (auto& [image_id, image] : images) {
- image.cam_from_world.translation = image.Center();
+ for (auto& [frame_id, frame] : frames) {
+ if (constrained_positions.find(frame_id) != constrained_positions.end())
+ frame.RigFromWorld().translation = CenterFromPose(frame.RigFromWorld());
}
return;
}
// Generate random positions for the cameras centers.
- for (auto& [image_id, image] : images) {
+ for (auto& [frame_id, frame] : frames) {
// Only set the cameras to be random if they are needed to be optimized
- if (constrained_positions.find(image_id) != constrained_positions.end())
- image.cam_from_world.translation =
+ if (constrained_positions.find(frame_id) != constrained_positions.end())
+ frame.RigFromWorld().translation =
100.0 * RandVector3d(random_generator_, -1, 1);
else
- image.cam_from_world.translation = image.Center();
+ frame.RigFromWorld().translation = CenterFromPose(frame.RigFromWorld());
}
- if (options_.verbose)
- LOG(INFO) << "Constrained positions: " << constrained_positions.size();
+ VLOG(2) << "Constrained positions: " << constrained_positions.size();
}
void GlobalPositioner::AddCameraToCameraConstraints(
const ViewGraph& view_graph, std::unordered_map& images) {
+ // For cam to cam constraint, only support the trivial frames now
+ for (const auto& [image_id, image] : images) {
+ if (!image.IsRegistered()) continue;
+ if (!image.HasTrivialFrame()) {
+ LOG(ERROR) << "Now, only trivial frames are supported for the camera to "
+ "camera constraints";
+ }
+ }
+
for (const auto& [pair_id, image_pair] : view_graph.image_pairs) {
if (image_pair.is_valid == false) continue;
const image_t image_id1 = image_pair.image_id1;
const image_t image_id2 = image_pair.image_id2;
if (images.find(image_id1) == images.end() ||
- images.find(image_id2) == images.end())
+ images.find(image_id2) == images.end()) {
continue;
+ }
- track_t counter = scales_.size();
- scales_.insert(std::make_pair(counter, 1));
+ CHECK_GE(scales_.capacity(), scales_.size())
+ << "Not enough capacity was reserved for the scales.";
+ double& scale = scales_.emplace_back(1);
- Eigen::Vector3d translation =
- -(images[image_id2].cam_from_world.rotation.inverse() *
+ const Eigen::Vector3d translation =
+ -(images[image_id2].CamFromWorld().rotation.inverse() *
image_pair.cam2_from_cam1.translation);
ceres::CostFunction* cost_function =
BATAPairwiseDirectionError::Create(translation);
problem_->AddResidualBlock(
cost_function,
- options_.loss_function.get(),
- images[image_id1].cam_from_world.translation.data(),
- images[image_id2].cam_from_world.translation.data(),
- &(scales_[counter]));
+ loss_function_.get(),
+ images[image_id1].frame_ptr->RigFromWorld().translation.data(),
+ images[image_id2].frame_ptr->RigFromWorld().translation.data(),
+ &scale);
- problem_->SetParameterLowerBound(&(scales_[counter]), 0, 1e-5);
+ problem_->SetParameterLowerBound(&scale, 0, 1e-5);
}
- if (options_.verbose)
- LOG(INFO) << problem_->NumResidualBlocks()
- << " camera to camera constraints were added to the position "
- "estimation problem.";
+ VLOG(2) << problem_->NumResidualBlocks()
+ << " camera to camera constraints were added to the position "
+ "estimation problem.";
}
void GlobalPositioner::AddPointToCameraConstraints(
+ std::unordered_map& rigs,
std::unordered_map& cameras,
+ std::unordered_map& frames,
std::unordered_map& images,
std::unordered_map